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 ( 온디바이스 AI Ollama 온프레미스 모델 직접 실행 오프라인 모드 {stats.requests}총 요청 {stats.avg_ms}ms평균 응답 {stats.cache_hits}캐시 히트 모델 선택 {MODELS.map(m => ( setSelectedModel(m.id)}> {m.name} {m.type} · {m.size} {!m.available && 다운로드 필요} ))} 쿼리 {loading ? '처리 중...' : '🤖 실행'} {result ? ( 응답 {result} ) : null} ); } 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 }, });