import React, { useCallback, useState } from "react"; import { Alert, Modal, Pressable, ScrollView, Text, TouchableOpacity, View, } from "react-native"; import * as Clipboard from "expo-clipboard"; import { useThemeColors } from "@/lib/theme"; import { clearVoicepeakDebugLogs, getVoicepeakDebugLogs, type VoicepeakDebugLogEntry, } from "@/lib/voicepeakDebugLog"; import { VoicepeakDebugAudioActions } from "@/components/Settings/VoicepeakDebugAudioActions"; const formatDebugLog = (log: VoicepeakDebugLogEntry) => JSON.stringify(log, null, 2); const formatAllDebugLogs = (logs: VoicepeakDebugLogEntry[]) => JSON.stringify( { exportedAt: new Date().toISOString(), retentionDays: 7, count: logs.length, logs, }, null, 2, ); export const VoicepeakDebugLogSection = () => { const { colors, fixed } = useThemeColors(); const [logs, setLogs] = useState([]); const [selectedLog, setSelectedLog] = useState( null, ); const [loading, setLoading] = useState(false); const [copied, setCopied] = useState(false); const [expanded, setExpanded] = useState(false); const refresh = useCallback(async () => { setLoading(true); try { setLogs(await getVoicepeakDebugLogs()); } catch (error) { console.warn("Failed to load Voicepeak debug logs", error); } finally { setLoading(false); } }, []); const toggleExpanded = useCallback(() => { setExpanded((current) => { const next = !current; if (next) void refresh(); return next; }); }, [refresh]); const copyText = useCallback(async (text: string) => { await Clipboard.setStringAsync(text); setCopied(true); setTimeout(() => setCopied(false), 1500); }, []); const clearLogs = useCallback(() => { Alert.alert( "音声作成ログを削除", "端末に保存されたVoicepeakデバッグ履歴をすべて削除します。", [ { text: "キャンセル", style: "cancel" }, { text: "削除", style: "destructive", onPress: () => { void clearVoicepeakDebugLogs().then(() => { setLogs([]); setSelectedLog(null); }); }, }, ], ); }, []); const borderColor = colors.borderSecondary ?? "#ccc"; const secondaryText = colors.textSecondary ?? colors.text; return ( Voicepeak デバッグログ {expanded ? `(${logs.length}件)` : ""} {expanded ? "閉じる ▲" : "表示する ▼"} {expanded && ( void refresh()}> {loading ? "読込中" : "更新"} )} {expanded && ( <> 音声作成APIへ送信したテキスト、HTTP結果、処理時間、応答サイズを端末内に保存します。 APIトークンは保存しません。履歴は7日後に自動削除されます。 void copyText(formatAllDebugLogs(logs))} style={{ flex: 1, paddingVertical: 10, borderRadius: 8, backgroundColor: logs.length > 0 ? fixed.primary : borderColor, }} > {copied ? "コピーしました" : "全履歴をコピー"} 0 ? "#d32f2f" : secondaryText }} > 削除 {logs.length === 0 ? ( 保存されたログはありません。 ) : ( logs.slice(0, 200).map((log) => { const statusColor = log.status === "success" ? "#2e7d32" : log.status === "error" ? "#d32f2f" : "#ed6c02"; return ( setSelectedLog(log)} style={{ paddingVertical: 11, borderTopWidth: 1, borderTopColor: borderColor, }} > {log.status === "success" ? "成功" : log.status === "error" ? "失敗" : "処理中"} {new Date(log.createdAt).toLocaleString("ja-JP")} {log.text} {log.codePointCount ?? Array.from(log.text).length}文字 {log.chunkIndex && log.totalChunks ? ` ・ 分割 ${log.chunkIndex}/${log.totalChunks}` : ""} {log.attemptNumber ? ` ・ 試行 ${log.attemptNumber}` : ""} {typeof log.httpStatus === "number" ? ` ・ HTTP ${log.httpStatus}` : ""} {log.errorCode ? ` ・ ${log.errorCode}` : ""} {log.cacheStatus ? ` ・ ${log.cacheStatus}` : ""} {typeof log.durationMilliseconds === "number" ? ` ・ ${log.durationMilliseconds}ms` : ""} ); }) )} )} setSelectedLog(null)} > setSelectedLog(null)} style={{ flex: 1, justifyContent: "center", padding: 20, backgroundColor: "rgba(0,0,0,0.55)", }} > event.stopPropagation()} style={{ maxHeight: "85%", padding: 16, borderRadius: 12, backgroundColor: colors.surface, }} > Voicepeakログ詳細 {selectedLog && ( )} {selectedLog ? formatDebugLog(selectedLog) : ""} selectedLog && void copyText(formatDebugLog(selectedLog)) } style={{ flex: 1, paddingVertical: 11, borderRadius: 8, backgroundColor: fixed.primary, }} > {copied ? "コピーしました" : "詳細をコピー"} setSelectedLog(null)} style={{ paddingHorizontal: 18, paddingVertical: 11, borderRadius: 8, borderWidth: 1, borderColor, }} > 閉じる ); };