- Add CPU/GPU memory monitoring endpoint and frontend widget - Add Amazon Bedrock model support (API key / inference profiles) plus model selector entries - Add Discord weather/laundry notification job (JMA forecast + LLM-generated message) - Add Discord bot that replies to mentions via the configured LLM, with weather-aware context - Add scheduled "useless tips" job with separate webhook and quiet-hours skip - Switch FastAPI startup to lifespan handler, log viewer, markdown rendering, and toast UI additions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
177 lines
5.8 KiB
Vue
177 lines
5.8 KiB
Vue
<script setup lang="ts">
|
|
import { ref, watch, onMounted } from 'vue'
|
|
import { fetchLogs, deleteLog, type ApiLog } from '../api/client'
|
|
import { useChatStore } from '../stores/chat'
|
|
|
|
const store = useChatStore()
|
|
|
|
const logs = ref<ApiLog[]>([])
|
|
const filterStatus = ref<'all' | 'ok' | 'error'>('all')
|
|
const filterSession = ref(false)
|
|
const expanded = ref<Set<number>>(new Set())
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
logs.value = await fetchLogs({
|
|
status: filterStatus.value === 'all' ? undefined : filterStatus.value,
|
|
session_id: filterSession.value && store.currentSessionId ? store.currentSessionId : undefined,
|
|
limit: 200,
|
|
})
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : 'エラーが発生しました'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function remove(id: number) {
|
|
await deleteLog(id)
|
|
logs.value = logs.value.filter((l) => l.id !== id)
|
|
}
|
|
|
|
function toggle(id: number) {
|
|
if (expanded.value.has(id)) expanded.value.delete(id)
|
|
else expanded.value.add(id)
|
|
}
|
|
|
|
function formatTime(iso: string) {
|
|
return new Date(iso).toLocaleString('ja-JP', { hour12: false })
|
|
}
|
|
|
|
function requestMessages(log: ApiLog): string {
|
|
try {
|
|
const msgs: { role: string; content: string }[] = JSON.parse(log.request_json)
|
|
return msgs.map((m) => `[${m.role}] ${m.content}`).join('\n\n')
|
|
} catch {
|
|
return log.request_json
|
|
}
|
|
}
|
|
|
|
watch([filterStatus, filterSession], load)
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<template>
|
|
<div class="log-viewer">
|
|
<div class="toolbar">
|
|
<span class="title">API ログ</span>
|
|
<div class="filters">
|
|
<label>
|
|
<input type="checkbox" v-model="filterSession" :disabled="!store.currentSessionId" />
|
|
現在のセッションのみ
|
|
</label>
|
|
<select v-model="filterStatus">
|
|
<option value="all">すべて</option>
|
|
<option value="ok">成功のみ</option>
|
|
<option value="error">エラーのみ</option>
|
|
</select>
|
|
</div>
|
|
<button class="reload-btn" @click="load" :disabled="loading">↻</button>
|
|
</div>
|
|
|
|
<div v-if="loading" class="empty">読み込み中...</div>
|
|
<div v-else-if="error" class="empty api-error">{{ error }}</div>
|
|
<div v-else-if="logs.length === 0" class="empty">ログがありません</div>
|
|
|
|
<ul v-else>
|
|
<li v-for="log in logs" :key="log.id" :class="{ 'has-error': !!log.error }">
|
|
<div class="log-header" @click="toggle(log.id)">
|
|
<span class="status-dot" :class="log.error ? 'err' : 'ok'" />
|
|
<span class="model">{{ log.model }}</span>
|
|
<span class="latency">{{ log.latency_ms != null ? log.latency_ms + ' ms' : '-' }}</span>
|
|
<span class="time">{{ formatTime(log.created_at) }}</span>
|
|
<button class="del-btn" @click.stop="remove(log.id)">✕</button>
|
|
<span class="arrow">{{ expanded.has(log.id) ? '▲' : '▼' }}</span>
|
|
</div>
|
|
|
|
<div v-if="expanded.has(log.id)" class="log-detail">
|
|
<section>
|
|
<h4>リクエスト</h4>
|
|
<pre>{{ requestMessages(log) }}</pre>
|
|
</section>
|
|
<section v-if="log.response_text">
|
|
<h4>レスポンス</h4>
|
|
<pre>{{ log.response_text }}</pre>
|
|
</section>
|
|
<section v-if="log.error" class="error-section">
|
|
<h4>エラー</h4>
|
|
<pre>{{ log.error }}</pre>
|
|
</section>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.log-viewer {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
background: #111;
|
|
}
|
|
.toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 12px;
|
|
border-bottom: 1px solid #2a2a2a;
|
|
flex-shrink: 0;
|
|
}
|
|
.title { font-size: 13px; font-weight: 600; color: #aaa; flex: 1; }
|
|
.filters { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #888; }
|
|
.filters label { display: flex; align-items: center; gap: 4px; cursor: pointer; }
|
|
.filters input { accent-color: #2563eb; cursor: pointer; }
|
|
select {
|
|
background: #2a2a2a; color: #ccc; border: 1px solid #444;
|
|
border-radius: 5px; padding: 2px 6px; font-size: 12px;
|
|
}
|
|
.reload-btn {
|
|
background: none; border: 1px solid #444; color: #aaa;
|
|
border-radius: 5px; padding: 2px 8px; cursor: pointer; font-size: 14px;
|
|
}
|
|
.reload-btn:hover:not(:disabled) { color: #fff; }
|
|
.empty { padding: 20px; text-align: center; color: #555; font-size: 13px; }
|
|
.api-error { color: #f87171 !important; }
|
|
ul { list-style: none; margin: 0; padding: 4px 0; overflow-y: auto; flex: 1; }
|
|
li { border-bottom: 1px solid #1e1e1e; }
|
|
li.has-error { border-left: 3px solid #7f1d1d; }
|
|
.log-header {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 8px 12px; cursor: pointer;
|
|
font-size: 12px;
|
|
}
|
|
.log-header:hover { background: #1a1a1a; }
|
|
.status-dot {
|
|
width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
|
|
}
|
|
.status-dot.ok { background: #22c55e; }
|
|
.status-dot.err { background: #ef4444; }
|
|
.model { color: #ccc; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.latency { color: #666; width: 60px; text-align: right; }
|
|
.time { color: #555; white-space: nowrap; }
|
|
.del-btn {
|
|
background: none; border: none; color: #555;
|
|
cursor: pointer; font-size: 11px; opacity: 0;
|
|
}
|
|
.log-header:hover .del-btn { opacity: 1; }
|
|
.arrow { color: #555; font-size: 10px; }
|
|
.log-detail {
|
|
padding: 10px 14px 14px;
|
|
display: flex; flex-direction: column; gap: 10px;
|
|
}
|
|
section h4 { font-size: 11px; color: #666; margin-bottom: 4px; text-transform: uppercase; }
|
|
pre {
|
|
background: #1a1a1a; color: #ccc; border-radius: 6px;
|
|
padding: 10px; font-size: 12px; line-height: 1.5;
|
|
white-space: pre-wrap; word-break: break-word; margin: 0;
|
|
max-height: 300px; overflow-y: auto;
|
|
}
|
|
.error-section pre { background: #1f0a0a; color: #f87171; }
|
|
</style>
|