Files
jrshikoku/components/Settings/VoicepeakDebugLogSection.tsx

374 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<VoicepeakDebugLogEntry[]>([]);
const [selectedLog, setSelectedLog] = useState<VoicepeakDebugLogEntry | null>(
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 (
<View
style={{
paddingHorizontal: 15,
paddingVertical: 18,
borderBottomWidth: 1,
borderBottomColor: borderColor,
backgroundColor: colors.surface,
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
marginBottom: expanded ? 8 : 0,
}}
>
<TouchableOpacity
accessibilityRole="button"
accessibilityState={{ expanded }}
onPress={toggleExpanded}
style={{
flex: 1,
flexDirection: "row",
alignItems: "center",
paddingVertical: 2,
}}
>
<Text
style={{
flex: 1,
fontSize: 16,
fontWeight: "600",
color: colors.text,
}}
>
Voicepeak デバッグログ
{expanded ? `${logs.length}件)` : ""}
</Text>
<Text style={{ color: fixed.primary, padding: 8 }}>
{expanded ? "閉じる ▲" : "表示する ▼"}
</Text>
</TouchableOpacity>
{expanded && (
<TouchableOpacity onPress={() => void refresh()}>
<Text style={{ color: fixed.primary, padding: 8 }}>
{loading ? "読込中" : "更新"}
</Text>
</TouchableOpacity>
)}
</View>
{expanded && (
<>
<Text
style={{
fontSize: 12,
lineHeight: 18,
color: secondaryText,
marginBottom: 12,
}}
>
音声作成APIへ送信したテキストHTTP結果、処理時間、応答サイズを端末内に保存します。
APIトークンは保存しません。履歴は7日後に自動削除されます。
</Text>
<View style={{ flexDirection: "row", gap: 8, marginBottom: 12 }}>
<TouchableOpacity
disabled={logs.length === 0}
onPress={() => void copyText(formatAllDebugLogs(logs))}
style={{
flex: 1,
paddingVertical: 10,
borderRadius: 8,
backgroundColor: logs.length > 0 ? fixed.primary : borderColor,
}}
>
<Text
style={{
color: "#fff",
textAlign: "center",
fontWeight: "600",
}}
>
{copied ? "コピーしました" : "全履歴をコピー"}
</Text>
</TouchableOpacity>
<TouchableOpacity
disabled={logs.length === 0}
onPress={clearLogs}
style={{
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 8,
borderWidth: 1,
borderColor,
}}
>
<Text
style={{ color: logs.length > 0 ? "#d32f2f" : secondaryText }}
>
削除
</Text>
</TouchableOpacity>
</View>
{logs.length === 0 ? (
<Text style={{ color: secondaryText, paddingVertical: 10 }}>
保存されたログはありません。
</Text>
) : (
logs.slice(0, 200).map((log) => {
const statusColor =
log.status === "success"
? "#2e7d32"
: log.status === "error"
? "#d32f2f"
: "#ed6c02";
return (
<TouchableOpacity
key={log.id}
onPress={() => setSelectedLog(log)}
style={{
paddingVertical: 11,
borderTopWidth: 1,
borderTopColor: borderColor,
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
marginBottom: 5,
}}
>
<Text style={{ color: statusColor, fontWeight: "700" }}>
{log.status === "success"
? "成功"
: log.status === "error"
? "失敗"
: "処理中"}
</Text>
<Text
style={{
flex: 1,
textAlign: "right",
color: secondaryText,
fontSize: 12,
}}
>
{new Date(log.createdAt).toLocaleString("ja-JP")}
</Text>
</View>
<Text
numberOfLines={2}
style={{ color: colors.text, fontSize: 13, lineHeight: 18 }}
>
{log.text}
</Text>
<Text
style={{
color: secondaryText,
fontSize: 11,
marginTop: 5,
}}
>
{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`
: ""}
</Text>
</TouchableOpacity>
);
})
)}
</>
)}
<Modal
visible={selectedLog !== null}
transparent
animationType="fade"
onRequestClose={() => setSelectedLog(null)}
>
<Pressable
onPress={() => setSelectedLog(null)}
style={{
flex: 1,
justifyContent: "center",
padding: 20,
backgroundColor: "rgba(0,0,0,0.55)",
}}
>
<Pressable
onPress={(event) => event.stopPropagation()}
style={{
maxHeight: "85%",
padding: 16,
borderRadius: 12,
backgroundColor: colors.surface,
}}
>
<Text
style={{
fontSize: 17,
fontWeight: "700",
color: colors.text,
marginBottom: 12,
}}
>
Voicepeakログ詳細
</Text>
{selectedLog && (
<VoicepeakDebugAudioActions
log={selectedLog}
onComplete={refresh}
/>
)}
<ScrollView>
<Text
selectable
style={{
color: colors.text,
fontSize: 12,
lineHeight: 18,
fontFamily: "monospace",
}}
>
{selectedLog ? formatDebugLog(selectedLog) : ""}
</Text>
</ScrollView>
<View style={{ flexDirection: "row", gap: 8, marginTop: 14 }}>
<TouchableOpacity
onPress={() =>
selectedLog && void copyText(formatDebugLog(selectedLog))
}
style={{
flex: 1,
paddingVertical: 11,
borderRadius: 8,
backgroundColor: fixed.primary,
}}
>
<Text
style={{
color: "#fff",
textAlign: "center",
fontWeight: "600",
}}
>
{copied ? "コピーしました" : "詳細をコピー"}
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setSelectedLog(null)}
style={{
paddingHorizontal: 18,
paddingVertical: 11,
borderRadius: 8,
borderWidth: 1,
borderColor,
}}
>
<Text style={{ color: colors.text }}>閉じる</Text>
</TouchableOpacity>
</View>
</Pressable>
</Pressable>
</Modal>
</View>
);
};