feat: add Koa2 BFF server, CLI management, sessions CLI integration, and logs page
- Add Koa2 BFF layer for API proxy, file upload, session management - Auto-check and enable api_server in ~/.hermes/config.yaml on startup - Integrate sessions with Hermes CLI (list, get, delete) - Add Logs page with level filtering, log file selection, and search - Add CLI commands: start/stop/restart/status for daemon management - Unify package.json for frontend and server dependencies - Default port changed to 8648 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,10 @@ const attachments = ref<Attachment[]>([])
|
||||
|
||||
const canSend = computed(() => inputText.value.trim() || attachments.value.length > 0)
|
||||
|
||||
function handleAttachClick() {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
function handleSend() {
|
||||
const text = inputText.value.trim()
|
||||
if (!text && attachments.value.length === 0) return
|
||||
@@ -39,10 +43,6 @@ function handleInput(e: Event) {
|
||||
el.style.height = Math.min(el.scrollHeight, 100) + 'px'
|
||||
}
|
||||
|
||||
function handleAttachClick() {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
function handleFileChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
const files = input.files
|
||||
@@ -128,7 +128,7 @@ function isImage(type: string): boolean {
|
||||
@input="handleInput"
|
||||
></textarea>
|
||||
<div class="input-actions">
|
||||
<!-- <NTooltip trigger="hover">
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<NButton quaternary size="small" @click="handleAttachClick" circle>
|
||||
<template #icon>
|
||||
@@ -137,7 +137,7 @@ function isImage(type: string): boolean {
|
||||
</NButton>
|
||||
</template>
|
||||
Attach files
|
||||
</NTooltip> -->
|
||||
</NTooltip>
|
||||
<NButton
|
||||
v-if="chatStore.isStreaming"
|
||||
size="small"
|
||||
|
||||
@@ -13,11 +13,11 @@ const message = useMessage()
|
||||
const showSessions = ref(true)
|
||||
|
||||
const sortedSessions = computed(() => {
|
||||
return [...chatStore.sessions].sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
return [...chatStore.sessions].sort((a, b) => b.createdAt - a.createdAt)
|
||||
})
|
||||
|
||||
const activeSessionLabel = computed(() =>
|
||||
chatStore.activeSession?.title || 'New Chat',
|
||||
chatStore.activeSession?.id || 'New Chat',
|
||||
)
|
||||
|
||||
function handleNewChat() {
|
||||
@@ -58,6 +58,8 @@ function formatTime(ts: number) {
|
||||
</NButton>
|
||||
</div>
|
||||
<div v-if="showSessions" class="session-items">
|
||||
<div v-if="chatStore.isLoadingSessions" class="session-loading">Loading...</div>
|
||||
<div v-else-if="sortedSessions.length === 0" class="session-empty">No sessions</div>
|
||||
<button
|
||||
v-for="s in sortedSessions"
|
||||
:key="s.id"
|
||||
@@ -66,8 +68,8 @@ function formatTime(ts: number) {
|
||||
@click="chatStore.switchSession(s.id)"
|
||||
>
|
||||
<div class="session-item-content">
|
||||
<span class="session-item-title">{{ s.title }}</span>
|
||||
<span class="session-item-time">{{ formatTime(s.updatedAt) }}</span>
|
||||
<span class="session-item-title">{{ s.id }}</span>
|
||||
<span class="session-item-time">{{ formatTime(s.createdAt) }}</span>
|
||||
</div>
|
||||
<NPopconfirm
|
||||
v-if="s.id !== chatStore.activeSessionId || sortedSessions.length > 1"
|
||||
@@ -169,6 +171,14 @@ function formatTime(ts: number) {
|
||||
padding: 0 6px 12px;
|
||||
}
|
||||
|
||||
.session-loading,
|
||||
.session-empty {
|
||||
padding: 16px 10px;
|
||||
font-size: 12px;
|
||||
color: $text-muted;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -46,12 +46,29 @@ function handleNav(key: string) {
|
||||
</svg>
|
||||
<span>Jobs</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="nav-item"
|
||||
:class="{ active: selectedKey === 'logs' }"
|
||||
@click="handleNav('logs')"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
<polyline points="10 9 9 9 8 9" />
|
||||
</svg>
|
||||
<span>Logs</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="status-indicator" :class="{ connected: appStore.connected, disconnected: !appStore.connected }">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">{{ appStore.connected ? 'Connected' : 'Disconnected' }}</span>
|
||||
<div class="status-row">
|
||||
<div class="status-indicator" :class="{ connected: appStore.connected, disconnected: !appStore.connected }">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">{{ appStore.connected ? 'Connected' : 'Disconnected' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-info">Hermes {{ appStore.serverVersion || 'v0.1.0' }}</div>
|
||||
</div>
|
||||
@@ -133,11 +150,17 @@ function handleNav(key: string) {
|
||||
border-top: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
|
||||
.status-dot {
|
||||
@@ -162,8 +185,9 @@ function handleNav(key: string) {
|
||||
}
|
||||
|
||||
.version-info {
|
||||
padding: 4px 12px;
|
||||
padding: 2px 12px 8px;
|
||||
font-size: 11px;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user