guardia-messenger/app/(tabs)/on_device_ai.tsx

112 lines
5.8 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { View, Text, ScrollView, TouchableOpacity, TextInput, StyleSheet, Switch } from 'react-native';
const MODELS = [
{ id: 'llama3', name: 'Llama 3 8B', size: '4.7GB', type: '범용', available: true },
{ id: 'codellama', name: 'CodeLlama 7B', size: '3.8GB', type: '코드', available: true },
{ id: 'nomic-embed', name: 'Nomic Embed', size: '0.3GB', type: '임베딩', available: true },
{ id: 'llava', name: 'LLaVA 7B', size: '4.1GB', type: '비전', available: false },
];
export default function OnDeviceAIScreen() {
const [query, setQuery] = useState('');
const [result, setResult] = useState('');
const [loading, setLoading] = useState(false);
const [selectedModel, setSelectedModel] = useState('llama3');
const [offlineMode, setOfflineMode] = useState(false);
const [stats, setStats] = useState({ requests: 0, avg_ms: 0, cache_hits: 0 });
const runQuery = async () => {
if (!query.trim()) return;
setLoading(true);
const start = Date.now();
try {
const r = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: selectedModel, prompt: query, stream: false }),
});
if (r.ok) {
const data = await r.json();
setResult(data.response || '응답 없음');
const elapsed = Date.now() - start;
setStats(prev => ({ requests: prev.requests + 1, avg_ms: Math.round((prev.avg_ms * prev.requests + elapsed) / (prev.requests + 1)), cache_hits: prev.cache_hits }));
}
} catch {
setResult(offlineMode ? '[오프라인] 캐시된 응답을 사용합니다.\n\n이 기기는 온디바이스 AI 추론 모드로 동작 중입니다.' : 'Ollama 서버에 연결할 수 없습니다.');
} finally { setLoading(false); }
};
return (
<ScrollView style={s.container}>
<Text style={s.title}> AI</Text>
<Text style={s.sub}>Ollama </Text>
<View style={s.card}>
<View style={s.row}><Text style={s.label}> </Text>
<Switch value={offlineMode} onValueChange={setOfflineMode} trackColor={{ true: '#00A0C8', false: '#333' }} />
</View>
<View style={s.statsRow}>
<View style={s.stat}><Text style={s.statVal}>{stats.requests}</Text><Text style={s.statLbl}> </Text></View>
<View style={s.stat}><Text style={s.statVal}>{stats.avg_ms}ms</Text><Text style={s.statLbl}> </Text></View>
<View style={s.stat}><Text style={s.statVal}>{stats.cache_hits}</Text><Text style={s.statLbl}> </Text></View>
</View>
</View>
<Text style={s.sectionTitle}> </Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ marginBottom: 16 }}>
{MODELS.map(m => (
<TouchableOpacity key={m.id} disabled={!m.available}
style={[s.modelChip, selectedModel === m.id && s.modelActive, !m.available && s.modelDisabled]}
onPress={() => setSelectedModel(m.id)}>
<Text style={s.modelName}>{m.name}</Text>
<Text style={s.modelMeta}>{m.type} · {m.size}</Text>
{!m.available && <Text style={s.modelStatus}> </Text>}
</TouchableOpacity>
))}
</ScrollView>
<Text style={s.sectionTitle}></Text>
<TextInput style={s.input} value={query} onChangeText={setQuery}
placeholder="질문을 입력하세요..." placeholderTextColor="#555"
multiline numberOfLines={3} textAlignVertical="top" />
<TouchableOpacity style={s.runBtn} onPress={runQuery} disabled={loading}>
<Text style={s.runBtnText}>{loading ? '처리 중...' : '🤖 실행'}</Text>
</TouchableOpacity>
{result ? (
<View style={s.resultBox}>
<Text style={s.resultLabel}></Text>
<Text style={s.resultText}>{result}</Text>
</View>
) : null}
</ScrollView>
);
}
const s = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0A0E1A', padding: 16 },
title: { color: '#fff', fontSize: 20, fontWeight: '700', marginBottom: 4 },
sub: { color: '#888', fontSize: 13, marginBottom: 16 },
card: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 16, marginBottom: 16, borderWidth: 1, borderColor: '#333' },
row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
label: { color: '#fff', fontSize: 15 },
statsRow: { flexDirection: 'row', justifyContent: 'space-around' },
stat: { alignItems: 'center' },
statVal: { color: '#00A0C8', fontSize: 18, fontWeight: '700' },
statLbl: { color: '#888', fontSize: 11 },
sectionTitle: { color: '#fff', fontSize: 15, fontWeight: '600', marginBottom: 10 },
modelChip: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 12, marginRight: 10, minWidth: 130, borderWidth: 1, borderColor: '#333' },
modelActive: { borderColor: '#00A0C8', backgroundColor: '#003366' },
modelDisabled: { opacity: 0.5 },
modelName: { color: '#fff', fontWeight: '600', marginBottom: 2 },
modelMeta: { color: '#888', fontSize: 11 },
modelStatus: { color: '#ff8800', fontSize: 11, marginTop: 4 },
input: { backgroundColor: '#1A1F2E', color: '#fff', borderRadius: 12, padding: 14, marginBottom: 12, minHeight: 80, borderWidth: 1, borderColor: '#333' },
runBtn: { backgroundColor: '#00A0C8', padding: 14, borderRadius: 12, alignItems: 'center', marginBottom: 16 },
runBtnText: { color: '#fff', fontWeight: '700', fontSize: 15 },
resultBox: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 16, borderWidth: 1, borderColor: '#003366' },
resultLabel: { color: '#00A0C8', fontWeight: '600', marginBottom: 8 },
resultText: { color: '#fff', lineHeight: 22 },
});