feat(v031): VisionSection UI — dropdown + 다시 감지 + 마지막 감지 시각

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
altair823
2026-05-10 04:59:19 +09:00
parent d03098cfac
commit 72e9b68923
6 changed files with 173 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { z } from 'zod';
import { inboxApi } from '../../api.js';
import { VisionSection } from './VisionSection.js';
const endpointSchema = z.string().url();
@@ -192,6 +193,7 @@ export function AiProviderSection(): React.ReactElement {
{recheckResult && (
<div style={{ fontSize: 12, marginTop: 8 }}>{recheckResult}</div>
)}
<VisionSection />
</div>
);
}

View File

@@ -0,0 +1,81 @@
import React, { useEffect, useState } from 'react';
import { inboxApi } from '../../api.js';
export function VisionSection(): React.ReactElement {
const [models, setModels] = useState<string[]>([]);
const [at, setAt] = useState<string | null>(null);
const [selected, setSelected] = useState<string | null>(null);
const [busy, setBusy] = useState<'select' | 'refresh' | null>(null);
const [feedback, setFeedback] = useState<string | null>(null);
async function load() {
const r = await inboxApi.getVisionModels();
setModels(r.models);
setAt(r.at);
setSelected(r.selected);
}
useEffect(() => {
void load();
}, []);
async function onSelect(value: string) {
const next = value === '' ? null : value;
setBusy('select');
setFeedback(null);
await inboxApi.setVisionModel(next);
setSelected(next);
setBusy(null);
}
async function onRefresh() {
setBusy('refresh');
setFeedback(null);
const r = await inboxApi.refreshVisionCache();
setBusy(null);
if (r.ok) {
await load();
setFeedback(`감지 완료 (${r.models.length}개)`);
} else {
setFeedback(`감지 실패: ${r.reason}`);
}
}
return (
<section style={{ marginTop: 16 }}>
<h4 style={{ fontSize: 13, marginBottom: 6 }}> ()</h4>
<div style={{ display: 'flex', gap: 6, alignItems: 'center', marginBottom: 6 }}>
<select
aria-label="이미지 분석 모델"
value={selected ?? ''}
onChange={(e) => { void onSelect(e.target.value); }}
disabled={busy !== null}
style={{ flex: 1, fontSize: 12, padding: '4px 8px', border: '1px solid #ccc', borderRadius: 4 }}
>
<option value="">()</option>
{models.map((m) => <option key={m} value={m}>{m}</option>)}
</select>
<button
onClick={() => { void onRefresh(); }}
disabled={busy !== null}
style={{ background: '#0a4b80', color: '#fff', border: 'none', cursor: 'pointer', fontSize: 12, padding: '4px 10px', borderRadius: 4 }}
>
{busy === 'refresh' ? '감지 중…' : '다시 감지'}
</button>
</div>
{at !== null && (
<div style={{ fontSize: 11, color: '#888' }}>
: {new Date(at).toLocaleString('ko-KR')}
</div>
)}
{feedback !== null && (
<div style={{ fontSize: 11, color: '#444', marginTop: 4 }}>{feedback}</div>
)}
{models.length === 0 && (
<div style={{ fontSize: 11, color: '#aaa', marginTop: 4 }}>
. Ollama vision "다시 감지" .
</div>
)}
</section>
);
}