Improve translation reliability and progress feedback
This commit is contained in:
parent
9439858b11
commit
51a6e845d1
20 changed files with 1194 additions and 216 deletions
|
|
@ -1,7 +1,9 @@
|
|||
/** Axios-based API client with JWT token management */
|
||||
|
||||
import axios from 'axios'
|
||||
import type { LLMModel, SessionResponse } from './types'
|
||||
import type { LLMModel, PhaseProgress, ProperNoun, SessionConfig, SessionResponse } from './types'
|
||||
|
||||
export const AUTH_UNAUTHORIZED_EVENT = 'llm-translator:unauthorized'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
|
|
@ -17,13 +19,13 @@ api.interceptors.request.use((config) => {
|
|||
return config
|
||||
})
|
||||
|
||||
// Auto-redirect on 401
|
||||
// Notify the auth store so App.vue can switch views without a router.
|
||||
api.interceptors.response.use(
|
||||
(res) => res,
|
||||
(err) => {
|
||||
if (err.response?.status === 401) {
|
||||
localStorage.removeItem('jwt_token')
|
||||
window.location.hash = '#/login'
|
||||
window.dispatchEvent(new Event(AUTH_UNAUTHORIZED_EVENT))
|
||||
}
|
||||
return Promise.reject(err)
|
||||
},
|
||||
|
|
@ -56,15 +58,7 @@ export async function listModels(): Promise<LLMModel[]> {
|
|||
|
||||
// ── Sessions ─────────────────────────────────────────
|
||||
|
||||
export async function createSession(params: {
|
||||
source_text: string
|
||||
source_language: string
|
||||
target_language: string
|
||||
model_phase1: string
|
||||
model_phase2: string
|
||||
model_phase3: string
|
||||
model_phase4: string
|
||||
}): Promise<{ session_id: string }> {
|
||||
export async function createSession(params: SessionConfig): Promise<{ session_id: string }> {
|
||||
const res = await api.post('/translate/create', params)
|
||||
return res.data
|
||||
}
|
||||
|
|
@ -74,13 +68,20 @@ export async function getSession(sessionId: string): Promise<SessionResponse> {
|
|||
return res.data
|
||||
}
|
||||
|
||||
export async function getSessionProgress(
|
||||
sessionId: string,
|
||||
): Promise<{ progress: PhaseProgress | null }> {
|
||||
const res = await api.get(`/sessions/${sessionId}/progress`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function deleteSession(sessionId: string): Promise<void> {
|
||||
await api.delete(`/sessions/${sessionId}`)
|
||||
}
|
||||
|
||||
export async function updateSession(
|
||||
sessionId: string,
|
||||
patch: Record<string, any>,
|
||||
patch: Partial<SessionConfig>,
|
||||
): Promise<SessionResponse> {
|
||||
const res = await api.patch(`/sessions/${sessionId}`, patch)
|
||||
return res.data
|
||||
|
|
@ -95,7 +96,7 @@ export async function runPhase1(sessionId: string): Promise<SessionResponse> {
|
|||
|
||||
export async function runPhase2(
|
||||
sessionId: string,
|
||||
proper_nouns: any[],
|
||||
proper_nouns: ProperNoun[],
|
||||
style: string,
|
||||
): Promise<SessionResponse> {
|
||||
const res = await api.post(`/translate/${sessionId}/phase2`, { proper_nouns, style })
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { me as apiMe, logout as apiLogout } from '../api'
|
||||
import { computed, onScopeDispose, ref } from 'vue'
|
||||
import { AUTH_UNAUTHORIZED_EVENT, me as apiMe, logout as apiLogout } from '../api'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user_id = ref<string | null>(null)
|
||||
|
|
@ -8,6 +8,14 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
|
||||
const isLoggedIn = computed(() => !!user_id.value && loaded.value)
|
||||
|
||||
function handleUnauthorized() {
|
||||
user_id.value = null
|
||||
loaded.value = true
|
||||
}
|
||||
|
||||
window.addEventListener(AUTH_UNAUTHORIZED_EVENT, handleUnauthorized)
|
||||
onScopeDispose(() => window.removeEventListener(AUTH_UNAUTHORIZED_EVENT, handleUnauthorized))
|
||||
|
||||
async function check() {
|
||||
if (loaded.value) return
|
||||
try {
|
||||
|
|
@ -23,7 +31,6 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
async function logout() {
|
||||
await apiLogout()
|
||||
user_id.value = null
|
||||
window.location.hash = '#/login'
|
||||
}
|
||||
|
||||
return { user_id, loaded, isLoggedIn, check, logout }
|
||||
|
|
|
|||
|
|
@ -1,24 +1,32 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { ProperNoun, TranslationSessionData, HistoryEntry, LLMModel } from '../types'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { isAxiosError } from 'axios'
|
||||
import type { HistoryEntry, LLMModel, Phase1Result, PhaseProgress, ProperNoun, SessionConfig, TranslationSessionData } from '../types'
|
||||
import * as api from '../api'
|
||||
import { useAuthStore } from './auth'
|
||||
|
||||
const HISTORY_KEY = 'llm_translator_history'
|
||||
const SESSION_KEY_PREFIX = 'llm_translator_session_'
|
||||
const MODELS_KEY = 'llm_translator_models'
|
||||
|
||||
interface SavedModels {
|
||||
p1?: string
|
||||
p3?: string
|
||||
p4?: string
|
||||
}
|
||||
|
||||
export const useTranslationStore = defineStore('translation', () => {
|
||||
const auth = useAuthStore()
|
||||
// ── State ────────────────────────────────────────
|
||||
const sessionId = ref<string | null>(null)
|
||||
const sourceText = ref('')
|
||||
const sourceLanguage = ref('auto')
|
||||
const targetLanguage = ref('한국어')
|
||||
const modelPhase1 = ref('')
|
||||
const modelPhase2 = ref('')
|
||||
const modelPhase3 = ref('')
|
||||
const modelPhase4 = ref('')
|
||||
|
||||
// Phase results
|
||||
const phase1Result = ref<{ translated: string; proper_nouns: ProperNoun[]; summary: string; style: string } | null>(null)
|
||||
const phase1Result = ref<Phase1Result | null>(null)
|
||||
const phase2ProperNouns = ref<ProperNoun[]>([])
|
||||
const phase2Style = ref('')
|
||||
const phase3Result = ref('')
|
||||
|
|
@ -30,22 +38,51 @@ export const useTranslationStore = defineStore('translation', () => {
|
|||
const error = ref<string | null>(null)
|
||||
const models = ref<LLMModel[]>([])
|
||||
const history = ref<HistoryEntry[]>([])
|
||||
const historySnapshot = ref(false)
|
||||
const progress = ref<PhaseProgress | null>(null)
|
||||
let progressTimer: ReturnType<typeof setInterval> | null = null
|
||||
let progressRequestPending = false
|
||||
|
||||
// ── Computed ─────────────────────────────────────
|
||||
const phase1Done = computed(() => !!phase1Result.value)
|
||||
const phase2Ready = computed(() => phase1Done.value) // can edit after P1
|
||||
const phase3Ready = computed(() => phase1Done.value && (phase2ProperNouns.value.length > 0 || phase2Style.value))
|
||||
const phase4Ready = computed(() => !!phase3Result.value)
|
||||
|
||||
const activeAbortController = ref<AbortController | null>(null)
|
||||
const phase2Ready = computed(() => phase1Done.value && !historySnapshot.value)
|
||||
const phase3Ready = computed(() => phase1Done.value && !historySnapshot.value)
|
||||
const phase4Ready = computed(() => !!phase3Result.value && !historySnapshot.value)
|
||||
|
||||
// ── Actions ──────────────────────────────────────
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string): string {
|
||||
if (isAxiosError<{ detail?: string }>(error)) {
|
||||
return error.response?.data?.detail || error.message || fallback
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback
|
||||
}
|
||||
|
||||
function loadSavedModels(): SavedModels {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(localStorage.getItem(MODELS_KEY) || '{}')
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
|
||||
const value = parsed as Record<string, unknown>
|
||||
return {
|
||||
p1: typeof value.p1 === 'string' ? value.p1 : undefined,
|
||||
p3: typeof value.p3 === 'string' ? value.p3 : undefined,
|
||||
p4: typeof value.p4 === 'string' ? value.p4 : undefined,
|
||||
}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function validModel(alias: string | undefined, fallback: string): string {
|
||||
if (models.value.length === 0) return alias || fallback
|
||||
return alias && models.value.some(model => model.alias === alias) ? alias : fallback
|
||||
}
|
||||
|
||||
async function loadModels() {
|
||||
try {
|
||||
models.value = await api.listModels()
|
||||
} catch (e: any) {
|
||||
console.error('Failed to load models:', e)
|
||||
} catch (error: unknown) {
|
||||
console.error('Failed to load models:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,24 +97,47 @@ export const useTranslationStore = defineStore('translation', () => {
|
|||
phase3Result.value = ''
|
||||
phase4Result.value = ''
|
||||
error.value = null
|
||||
historySnapshot.value = false
|
||||
|
||||
// Restore model selections from localStorage
|
||||
const savedModels = JSON.parse(localStorage.getItem('llm_translator_models') || '{}')
|
||||
modelPhase1.value = savedModels.p1 || models.value[0]?.alias || ''
|
||||
modelPhase2.value = savedModels.p2 || models.value[0]?.alias || ''
|
||||
modelPhase3.value = savedModels.p3 || models.value[0]?.alias || ''
|
||||
modelPhase4.value = savedModels.p4 || (models.value[1]?.alias || models.value[0]?.alias || '')
|
||||
const savedModels = loadSavedModels()
|
||||
const defaultModel = models.value[0]?.alias || ''
|
||||
modelPhase1.value = validModel(savedModels.p1, defaultModel)
|
||||
modelPhase3.value = validModel(savedModels.p3, defaultModel)
|
||||
modelPhase4.value = validModel(savedModels.p4, models.value[1]?.alias || defaultModel)
|
||||
}
|
||||
|
||||
function saveModelSelections() {
|
||||
localStorage.setItem('llm_translator_models', JSON.stringify({
|
||||
p1: modelPhase1.value,
|
||||
p2: modelPhase2.value,
|
||||
p3: modelPhase3.value,
|
||||
p4: modelPhase4.value,
|
||||
}))
|
||||
try {
|
||||
localStorage.setItem(MODELS_KEY, JSON.stringify({
|
||||
p1: modelPhase1.value,
|
||||
p3: modelPhase3.value,
|
||||
p4: modelPhase4.value,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Failed to save model selections:', error)
|
||||
}
|
||||
}
|
||||
|
||||
watch([modelPhase1, modelPhase3, modelPhase4], saveModelSelections)
|
||||
watch([sourceText, sourceLanguage, targetLanguage, modelPhase1], () => {
|
||||
phase1Result.value = null
|
||||
phase2ProperNouns.value = []
|
||||
phase2Style.value = ''
|
||||
phase3Result.value = ''
|
||||
phase4Result.value = ''
|
||||
}, { flush: 'sync' })
|
||||
watch(modelPhase3, () => {
|
||||
phase3Result.value = ''
|
||||
phase4Result.value = ''
|
||||
}, { flush: 'sync' })
|
||||
watch(modelPhase4, () => {
|
||||
phase4Result.value = ''
|
||||
}, { flush: 'sync' })
|
||||
watch([phase2ProperNouns, phase2Style], () => {
|
||||
phase3Result.value = ''
|
||||
phase4Result.value = ''
|
||||
}, { deep: true, flush: 'sync' })
|
||||
|
||||
async function ensureSession() {
|
||||
if (!sessionId.value) {
|
||||
const res = await api.createSession(getSessionParams())
|
||||
|
|
@ -85,13 +145,13 @@ export const useTranslationStore = defineStore('translation', () => {
|
|||
}
|
||||
}
|
||||
|
||||
function getSessionParams() {
|
||||
function getSessionParams(): SessionConfig {
|
||||
return {
|
||||
source_text: sourceText.value,
|
||||
source_language: sourceLanguage.value,
|
||||
target_language: targetLanguage.value,
|
||||
model_phase1: modelPhase1.value,
|
||||
model_phase2: modelPhase2.value,
|
||||
model_phase2: '',
|
||||
model_phase3: modelPhase3.value,
|
||||
model_phase4: modelPhase4.value,
|
||||
}
|
||||
|
|
@ -99,10 +159,44 @@ export const useTranslationStore = defineStore('translation', () => {
|
|||
|
||||
async function syncToServer() {
|
||||
if (!sessionId.value) return
|
||||
await api.updateSession(sessionId.value, getSessionParams())
|
||||
}
|
||||
|
||||
async function submitPhase2() {
|
||||
if (!sessionId.value) throw new Error('세션 생성 실패')
|
||||
const res = await api.runPhase2(
|
||||
sessionId.value,
|
||||
phase2ProperNouns.value.map(pn => ({ ...pn, final: pn.final || pn.suggested })),
|
||||
phase2Style.value,
|
||||
)
|
||||
phase2ProperNouns.value = res.data.phase2_proper_nouns || []
|
||||
phase2Style.value = res.data.phase2_style || ''
|
||||
}
|
||||
|
||||
async function pollProgress() {
|
||||
if (!sessionId.value || progressRequestPending) return
|
||||
progressRequestPending = true
|
||||
try {
|
||||
await api.updateSession(sessionId.value, getSessionParams())
|
||||
} catch (e: any) {
|
||||
console.error('Sync failed:', e)
|
||||
const response = await api.getSessionProgress(sessionId.value)
|
||||
progress.value = response.progress
|
||||
} catch {
|
||||
// The phase request handles user-visible errors; polling is best-effort only.
|
||||
} finally {
|
||||
progressRequestPending = false
|
||||
}
|
||||
}
|
||||
|
||||
function startProgressPolling() {
|
||||
stopProgressPolling()
|
||||
progress.value = null
|
||||
void pollProgress()
|
||||
progressTimer = setInterval(() => void pollProgress(), 750)
|
||||
}
|
||||
|
||||
function stopProgressPolling() {
|
||||
if (progressTimer !== null) {
|
||||
clearInterval(progressTimer)
|
||||
progressTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,21 +215,25 @@ export const useTranslationStore = defineStore('translation', () => {
|
|||
loading.value = true
|
||||
currentPhaseLoading.value = 1
|
||||
error.value = null
|
||||
activeAbortController.value = new AbortController()
|
||||
|
||||
phase3Result.value = ''
|
||||
phase4Result.value = ''
|
||||
try {
|
||||
await ensureSession()
|
||||
if (!sessionId.value) throw new Error('세션 생성 실패')
|
||||
await syncToServer()
|
||||
startProgressPolling()
|
||||
const res = await api.runPhase1(sessionId.value)
|
||||
phase1Result.value = res.data.phase1_result
|
||||
phase2ProperNouns.value = res.data.phase2_proper_nouns || []
|
||||
phase2Style.value = res.data.phase2_style || ''
|
||||
} catch (e: any) {
|
||||
error.value = e.response?.data?.detail || e.message || 'Phase 1 실행 실패'
|
||||
historySnapshot.value = false
|
||||
saveToHistory()
|
||||
} catch (caughtError: unknown) {
|
||||
error.value = getErrorMessage(caughtError, 'Phase 1 실행 실패')
|
||||
} finally {
|
||||
stopProgressPolling()
|
||||
loading.value = false
|
||||
currentPhaseLoading.value = null
|
||||
activeAbortController.value = null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -146,18 +244,14 @@ export const useTranslationStore = defineStore('translation', () => {
|
|||
loading.value = true
|
||||
currentPhaseLoading.value = 2
|
||||
error.value = null
|
||||
phase3Result.value = ''
|
||||
phase4Result.value = ''
|
||||
|
||||
try {
|
||||
const res = await api.runPhase2(
|
||||
sessionId.value,
|
||||
phase2ProperNouns.value.map(pn => ({ ...pn, final: pn.final || pn.suggested })),
|
||||
phase2Style.value,
|
||||
)
|
||||
// Update local state from server response
|
||||
phase2ProperNouns.value = res.data.phase2_proper_nouns || []
|
||||
phase2Style.value = res.data.phase2_style || ''
|
||||
} catch (e: any) {
|
||||
error.value = e.response?.data?.detail || e.message || 'Phase 2 확인 실패'
|
||||
await submitPhase2()
|
||||
saveToHistory()
|
||||
} catch (caughtError: unknown) {
|
||||
error.value = getErrorMessage(caughtError, 'Phase 2 확인 실패')
|
||||
} finally {
|
||||
loading.value = false
|
||||
currentPhaseLoading.value = null
|
||||
|
|
@ -177,19 +271,25 @@ export const useTranslationStore = defineStore('translation', () => {
|
|||
loading.value = true
|
||||
currentPhaseLoading.value = 3
|
||||
error.value = null
|
||||
activeAbortController.value = new AbortController()
|
||||
phase3Result.value = ''
|
||||
phase4Result.value = ''
|
||||
|
||||
try {
|
||||
await ensureSession()
|
||||
if (!sessionId.value) throw new Error('세션 생성 실패')
|
||||
await syncToServer()
|
||||
await submitPhase2()
|
||||
saveToHistory()
|
||||
startProgressPolling()
|
||||
const res = await api.runPhase3(sessionId.value)
|
||||
phase3Result.value = res.data.phase3_result || ''
|
||||
} catch (e: any) {
|
||||
error.value = e.response?.data?.detail || e.message || 'Phase 3 실행 실패'
|
||||
saveToHistory()
|
||||
} catch (caughtError: unknown) {
|
||||
error.value = getErrorMessage(caughtError, 'Phase 3 실행 실패')
|
||||
} finally {
|
||||
stopProgressPolling()
|
||||
loading.value = false
|
||||
currentPhaseLoading.value = null
|
||||
activeAbortController.value = null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -202,19 +302,21 @@ export const useTranslationStore = defineStore('translation', () => {
|
|||
loading.value = true
|
||||
currentPhaseLoading.value = 4
|
||||
error.value = null
|
||||
activeAbortController.value = new AbortController()
|
||||
|
||||
try {
|
||||
await ensureSession()
|
||||
if (!sessionId.value) throw new Error('세션 생성 실패')
|
||||
await syncToServer()
|
||||
startProgressPolling()
|
||||
const res = await api.runPhase4(sessionId.value)
|
||||
phase4Result.value = res.data.phase4_result || ''
|
||||
} catch (e: any) {
|
||||
error.value = e.response?.data?.detail || e.message || 'Phase 4 실행 실패'
|
||||
saveToHistory()
|
||||
} catch (caughtError: unknown) {
|
||||
error.value = getErrorMessage(caughtError, 'Phase 4 실행 실패')
|
||||
} finally {
|
||||
stopProgressPolling()
|
||||
loading.value = false
|
||||
currentPhaseLoading.value = null
|
||||
activeAbortController.value = null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -239,32 +341,43 @@ export const useTranslationStore = defineStore('translation', () => {
|
|||
// Keep max 50 entries
|
||||
if (history.value.length > 50) history.value.pop()
|
||||
|
||||
localStorage.setItem(HISTORY_KEY, JSON.stringify(history.value))
|
||||
try {
|
||||
localStorage.setItem(historyStorageKey(), JSON.stringify(history.value))
|
||||
} catch (error) {
|
||||
console.error('Failed to save translation history:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function historyStorageKey(): string {
|
||||
return `${HISTORY_KEY}:${auth.user_id || 'anonymous'}`
|
||||
}
|
||||
|
||||
function loadHistory() {
|
||||
try {
|
||||
const raw = localStorage.getItem(HISTORY_KEY) || '[]'
|
||||
const key = historyStorageKey()
|
||||
const raw = localStorage.getItem(key) || '[]'
|
||||
history.value = JSON.parse(raw) as HistoryEntry[]
|
||||
localStorage.removeItem(HISTORY_KEY)
|
||||
} catch {
|
||||
history.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreFromHistory(entry: HistoryEntry) {
|
||||
sessionId.value = entry.id
|
||||
function restoreFromHistory(entry: HistoryEntry) {
|
||||
sessionId.value = null
|
||||
sourceText.value = entry.data.source_text
|
||||
sourceLanguage.value = entry.data.source_language
|
||||
targetLanguage.value = entry.data.target_language
|
||||
modelPhase1.value = entry.data.model_phase1
|
||||
modelPhase2.value = entry.data.model_phase2
|
||||
modelPhase3.value = entry.data.model_phase3
|
||||
modelPhase4.value = entry.data.model_phase4
|
||||
const defaultModel = models.value[0]?.alias || ''
|
||||
modelPhase1.value = validModel(entry.data.model_phase1, defaultModel)
|
||||
modelPhase3.value = validModel(entry.data.model_phase3, defaultModel)
|
||||
modelPhase4.value = validModel(entry.data.model_phase4, models.value[1]?.alias || defaultModel)
|
||||
phase1Result.value = entry.data.phase1_result
|
||||
phase2ProperNouns.value = entry.data.phase2_proper_nouns || []
|
||||
phase2Style.value = entry.data.phase2_style || ''
|
||||
phase3Result.value = entry.data.phase3_result || ''
|
||||
phase4Result.value = entry.data.phase4_result || ''
|
||||
historySnapshot.value = true
|
||||
|
||||
// Save model selections for future sessions
|
||||
saveModelSelections()
|
||||
|
|
@ -276,7 +389,7 @@ export const useTranslationStore = defineStore('translation', () => {
|
|||
source_language: sourceLanguage.value,
|
||||
target_language: targetLanguage.value,
|
||||
model_phase1: modelPhase1.value,
|
||||
model_phase2: modelPhase2.value,
|
||||
model_phase2: '',
|
||||
model_phase3: modelPhase3.value,
|
||||
model_phase4: modelPhase4.value,
|
||||
phase1_result: phase1Result.value || null,
|
||||
|
|
@ -309,7 +422,7 @@ export const useTranslationStore = defineStore('translation', () => {
|
|||
// Add simple markdown formatting — frontmatter with metadata
|
||||
const md = `---
|
||||
source_language: ${sourceLanguage.value}
|
||||
target_language: ${target_language.value}
|
||||
target_language: ${targetLanguage.value}
|
||||
timestamp: ${new Date().toISOString()}
|
||||
---\n\n${content}`
|
||||
downloadTxt(filename.replace('.txt', '.md'), md)
|
||||
|
|
@ -318,10 +431,10 @@ timestamp: ${new Date().toISOString()}
|
|||
return {
|
||||
// State
|
||||
sessionId, sourceText, sourceLanguage, targetLanguage,
|
||||
modelPhase1, modelPhase2, modelPhase3, modelPhase4,
|
||||
modelPhase1, modelPhase3, modelPhase4,
|
||||
phase1Result, phase2ProperNouns, phase2Style,
|
||||
phase3Result, phase4Result,
|
||||
loading, currentPhaseLoading, error, models, history,
|
||||
loading, currentPhaseLoading, error, models, history, progress,
|
||||
|
||||
// Computed
|
||||
phase1Done, phase2Ready, phase3Ready, phase4Ready,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,15 @@ export interface Phase1Result {
|
|||
proper_nouns: ProperNoun[]
|
||||
summary: string
|
||||
style: string
|
||||
warnings?: string[]
|
||||
}
|
||||
|
||||
export interface PhaseProgress {
|
||||
phase: number
|
||||
chunk: number
|
||||
total_chunks: number
|
||||
status: string
|
||||
preview: string
|
||||
}
|
||||
|
||||
export interface TranslationSessionData {
|
||||
|
|
@ -28,12 +37,26 @@ export interface TranslationSessionData {
|
|||
model_phase3: string
|
||||
model_phase4: string
|
||||
phase1_result: Phase1Result | null
|
||||
phase1_chunks?: Array<{ source_text: string; translated: string }>
|
||||
phase2_proper_nouns: ProperNoun[]
|
||||
phase2_style: string
|
||||
phase3_result: string
|
||||
phase3_chunks?: string[]
|
||||
phase4_result: string
|
||||
progress?: PhaseProgress | null
|
||||
}
|
||||
|
||||
export type SessionConfig = Pick<
|
||||
TranslationSessionData,
|
||||
| 'source_text'
|
||||
| 'source_language'
|
||||
| 'target_language'
|
||||
| 'model_phase1'
|
||||
| 'model_phase2'
|
||||
| 'model_phase3'
|
||||
| 'model_phase4'
|
||||
>
|
||||
|
||||
export interface SessionResponse {
|
||||
session_id: string
|
||||
data: TranslationSessionData
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { isAxiosError } from 'axios'
|
||||
import * as api from '../api'
|
||||
|
||||
const id = ref('')
|
||||
|
|
@ -14,8 +15,10 @@ async function handleLogin() {
|
|||
try {
|
||||
await api.login(id.value, password.value)
|
||||
window.location.reload()
|
||||
} catch (e: any) {
|
||||
errorMessage.value = e.response?.data?.detail || '로그인에 실패했습니다'
|
||||
} catch (error: unknown) {
|
||||
errorMessage.value = isAxiosError<{ detail?: string }>(error)
|
||||
? error.response?.data?.detail || '로그인에 실패했습니다'
|
||||
: '로그인에 실패했습니다'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,6 @@ function handleNewTranslation() {
|
|||
<section class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-5">
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<LLMSelector label="Phase 1 — 초벌번역" v-model:value="store.modelPhase1" :models="store.models" />
|
||||
<LLMSelector label="Phase 2 — 고유명사" v-model:value="store.modelPhase2" :models="store.models" />
|
||||
<LLMSelector label="Phase 3 — 재번역" v-model:value="store.modelPhase3" :models="store.models" />
|
||||
<LLMSelector label="Phase 4 — 마무리" v-model:value="store.modelPhase4" :models="store.models" />
|
||||
|
||||
|
|
@ -208,16 +207,32 @@ function handleNewTranslation() {
|
|||
</div>
|
||||
|
||||
<!-- Loading Indicator -->
|
||||
<div v-if="store.loading && !store.error" class="flex items-center gap-2 text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 px-4 py-3 rounded-lg text-sm">
|
||||
<svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
{{ phaseLabel(store.currentPhaseLoading || 0) }} 진행 중...
|
||||
<div v-if="store.loading && !store.error" class="text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/20 px-4 py-3 rounded-lg text-sm space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
<span>{{ phaseLabel(store.currentPhaseLoading || 0) }} 진행 중</span>
|
||||
<span v-if="store.progress" class="font-medium">
|
||||
· 조각 {{ store.progress.chunk }}/{{ store.progress.total_chunks }} · {{ store.progress.status }}
|
||||
</span>
|
||||
<span v-else class="text-blue-500 dark:text-blue-400">· 서버 응답 대기 중</span>
|
||||
</div>
|
||||
<pre
|
||||
v-if="store.progress?.preview"
|
||||
class="max-h-44 overflow-y-auto whitespace-pre-wrap break-words rounded-lg border border-blue-200 dark:border-blue-800 bg-white/70 dark:bg-gray-900/60 p-3 text-xs leading-relaxed text-gray-600 dark:text-gray-300"
|
||||
>{{ store.progress.preview }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Phase 2: Proper Noun Editor -->
|
||||
<section v-if="store.phase1Done">
|
||||
<div
|
||||
v-if="store.phase1Result?.warnings?.length"
|
||||
class="mb-4 rounded-xl border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20 px-4 py-3 text-sm text-amber-800 dark:text-amber-300"
|
||||
>
|
||||
<p v-for="warning in store.phase1Result.warnings" :key="warning">{{ warning }}</p>
|
||||
</div>
|
||||
<ProperNounsEditor
|
||||
:proper_nouns="store.phase2ProperNouns"
|
||||
:style="store.phase2Style"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue