feat(frontend): 优化聊天界面与监控交互体验
This commit is contained in:
parent
8be1c37b6a
commit
4124faa040
@ -116,11 +116,7 @@
|
||||
:format-rate="formatRate"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="titleRibbonVisible"
|
||||
class="conversation-ribbon"
|
||||
ref="titleRibbon"
|
||||
>
|
||||
<div v-if="titleRibbonVisible" class="conversation-ribbon" ref="titleRibbon">
|
||||
<button
|
||||
type="button"
|
||||
class="conversation-ribbon__selector"
|
||||
@ -135,18 +131,23 @@
|
||||
<span class="selector-mode">{{ headerRunModeLabel }}</span>
|
||||
</span>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" class="selector-caret">
|
||||
<path d="M7 10l5 5 5-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path
|
||||
d="M7 10l5 5 5-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<span class="conversation-ribbon__text">{{ titleTypingText || currentConversationTitle }}</span>
|
||||
<span class="conversation-ribbon__text">{{
|
||||
titleTypingText || currentConversationTitle
|
||||
}}</span>
|
||||
|
||||
<transition name="header-menu">
|
||||
<div
|
||||
v-if="headerMenuOpen"
|
||||
class="model-mode-dropdown"
|
||||
ref="headerMenu"
|
||||
>
|
||||
<div v-if="headerMenuOpen" class="model-mode-dropdown" ref="headerMenu">
|
||||
<div class="dropdown-column" data-tutorial="header-model-options">
|
||||
<div class="dropdown-title">模型</div>
|
||||
<div class="dropdown-list dropdown-list--models">
|
||||
@ -204,7 +205,7 @@
|
||||
:format-search-time="formatSearchTime"
|
||||
:format-search-domains="formatSearchDomains"
|
||||
/>
|
||||
<VirtualMonitorSurface v-show="chatDisplayMode === 'monitor'" />
|
||||
<VirtualMonitorSurface v-if="chatDisplayMode === 'monitor'" />
|
||||
|
||||
<div v-if="blankHeroActive" class="blank-hero-overlay">
|
||||
<span class="icon icon-lg" :style="iconStyle('bot')" aria-hidden="true"></span>
|
||||
@ -227,7 +228,13 @@
|
||||
<path d="M8 11h8a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z" />
|
||||
<path d="M9 11V8a3 3 0 0 1 6 0v3" />
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" aria-hidden="true" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg
|
||||
v-else
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 5v12" />
|
||||
<path d="M7 13l5 5 5-5" />
|
||||
</svg>
|
||||
@ -298,10 +305,14 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div v-if="!isMobileViewport" class="resize-handle" @mousedown="startResize('right', $event)"></div>
|
||||
<div
|
||||
v-if="!isMobileViewport"
|
||||
class="resize-handle"
|
||||
@mousedown="startResize('right', $event)"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<PersonalizationDrawer />
|
||||
<PersonalizationDrawer v-if="personalizationStore.visible" />
|
||||
<transition name="overlay-fade">
|
||||
<ImagePicker
|
||||
v-if="imagePickerOpen"
|
||||
@ -354,7 +365,7 @@
|
||||
</transition>
|
||||
<SubAgentActivityDialog />
|
||||
<BackgroundCommandDialog />
|
||||
<TutorialOverlay />
|
||||
<TutorialOverlay v-if="tutorialStore.running" />
|
||||
<NewUserTutorialPrompt
|
||||
:visible="tutorialPromptVisible"
|
||||
:username="tutorialPromptUsername"
|
||||
@ -370,7 +381,13 @@
|
||||
ref="mobilePanelTrigger"
|
||||
>
|
||||
<div class="mobile-panel-topbar">
|
||||
<button type="button" class="mobile-panel-fab" data-tutorial="mobile-menu-trigger" aria-label="切换工作区" @click="toggleMobileOverlayMenu">
|
||||
<button
|
||||
type="button"
|
||||
class="mobile-panel-fab"
|
||||
data-tutorial="mobile-menu-trigger"
|
||||
aria-label="切换工作区"
|
||||
@click="toggleMobileOverlayMenu"
|
||||
>
|
||||
<img :src="mobilePanelIcon" alt="" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
@ -409,8 +426,18 @@
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M8 8.5h8" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
|
||||
<path d="M8 11.5h5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
|
||||
<path
|
||||
d="M8 8.5h8"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 11.5h5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div class="item-label">对话记录</div>
|
||||
</button>
|
||||
@ -421,7 +448,12 @@
|
||||
aria-label="工作文件"
|
||||
@click="openMobileOverlay('workspace')"
|
||||
>
|
||||
<img class="mobile-menu-icon" :src="mobileMenuIcons.workspace" alt="" aria-hidden="true" />
|
||||
<img
|
||||
class="mobile-menu-icon"
|
||||
:src="mobileMenuIcons.workspace"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="item-label">工作文件</div>
|
||||
</button>
|
||||
<button
|
||||
@ -431,7 +463,12 @@
|
||||
aria-label="个人空间"
|
||||
@click="handleMobilePersonalClick"
|
||||
>
|
||||
<img class="mobile-menu-icon" :src="mobileMenuIcons.personal" alt="" aria-hidden="true" />
|
||||
<img
|
||||
class="mobile-menu-icon"
|
||||
:src="mobileMenuIcons.personal"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="item-label">个人空间</div>
|
||||
</button>
|
||||
<button
|
||||
@ -441,7 +478,12 @@
|
||||
aria-label="新建对话"
|
||||
@click="createNewConversation"
|
||||
>
|
||||
<img class="mobile-menu-icon" :src="mobileMenuIcons.newChat" alt="" aria-hidden="true" />
|
||||
<img
|
||||
class="mobile-menu-icon"
|
||||
:src="mobileMenuIcons.newChat"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="item-label">新建对话</div>
|
||||
</button>
|
||||
<button
|
||||
@ -451,7 +493,12 @@
|
||||
aria-label="刷新页面"
|
||||
@click="handleMobileRefreshClick"
|
||||
>
|
||||
<img class="mobile-menu-icon" :src="mobileMenuIcons.refresh" alt="" aria-hidden="true" />
|
||||
<img
|
||||
class="mobile-menu-icon"
|
||||
:src="mobileMenuIcons.refresh"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="item-label">刷新页面</div>
|
||||
</button>
|
||||
</div>
|
||||
@ -537,7 +584,13 @@
|
||||
@click.self="closeMobileOverlay"
|
||||
>
|
||||
<div class="mobile-panel-sheet mobile-panel-sheet--workspace">
|
||||
<button type="button" class="mobile-overlay-close" data-tutorial="mobile-workspace-close" aria-label="关闭面板" @click="closeMobileOverlay">
|
||||
<button
|
||||
type="button"
|
||||
class="mobile-overlay-close"
|
||||
data-tutorial="mobile-workspace-close"
|
||||
aria-label="关闭面板"
|
||||
@click="closeMobileOverlay"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<LeftPanel
|
||||
@ -561,14 +614,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import appOptions from './app';
|
||||
import VirtualMonitorSurface from './components/chat/VirtualMonitorSurface.vue';
|
||||
import VideoPicker from './components/overlay/VideoPicker.vue';
|
||||
import { usePersonalizationStore } from './stores/personalization';
|
||||
import { useTutorialStore } from './stores/tutorial';
|
||||
|
||||
const VirtualMonitorSurface = defineAsyncComponent(
|
||||
() => import('./components/chat/VirtualMonitorSurface.vue')
|
||||
);
|
||||
const personalizationStore = usePersonalizationStore();
|
||||
const tutorialStore = useTutorialStore();
|
||||
|
||||
const mobilePanelIcon = new URL('../icons/align-left.svg', import.meta.url).href;
|
||||
const isAppShell =
|
||||
typeof window !== 'undefined' &&
|
||||
(new URLSearchParams(window.location.search).has('app_shell') || Boolean((window as any)?.AndroidThemeBridge));
|
||||
(new URLSearchParams(window.location.search).has('app_shell') ||
|
||||
Boolean((window as any)?.AndroidThemeBridge));
|
||||
const mobileMenuIcons = {
|
||||
workspace: new URL('../icons/folder.svg', import.meta.url).href,
|
||||
personal: new URL('../icons/user.svg', import.meta.url).href,
|
||||
|
||||
@ -20,7 +20,14 @@
|
||||
<button type="button" :disabled="secondaryLoading" @click="handleVerifySecondary">
|
||||
{{ secondaryLoading ? '校验中...' : '确认进入' }}
|
||||
</button>
|
||||
<button type="button" class="ghost-btn" :disabled="secondaryLoading" @click="checkSecondary">重新检测</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ghost-btn"
|
||||
:disabled="secondaryLoading"
|
||||
@click="checkSecondary"
|
||||
>
|
||||
重新检测
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="secondaryError" class="secondary-error">{{ secondaryError }}</p>
|
||||
</div>
|
||||
@ -31,9 +38,7 @@
|
||||
<p>最近更新:{{ timeAgo(snapshot?.generated_at || overview.generated_at) }}</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<label>
|
||||
<input type="checkbox" v-model="autoRefresh" /> 自动刷新
|
||||
</label>
|
||||
<label> <input type="checkbox" v-model="autoRefresh" /> 自动刷新 </label>
|
||||
<button type="button" :disabled="refreshing" @click="handleManualRefresh">
|
||||
{{ refreshing ? '刷新中...' : '立即刷新' }}
|
||||
</button>
|
||||
@ -215,7 +220,10 @@
|
||||
<td>{{ formatPercentNumber(item.stats?.memory?.percent) }}</td>
|
||||
<td>{{ formatBytes(item.stats?.memory?.used_bytes) }}</td>
|
||||
<td>{{ formatBytes(item.stats?.memory?.limit_bytes) }}</td>
|
||||
<td>{{ formatBytes(item.stats?.net_io?.rx_bytes) }} / {{ formatBytes(item.stats?.net_io?.tx_bytes) }}</td>
|
||||
<td>
|
||||
{{ formatBytes(item.stats?.net_io?.rx_bytes) }} /
|
||||
{{ formatBytes(item.stats?.net_io?.tx_bytes) }}
|
||||
</td>
|
||||
<td>
|
||||
<span :class="['status-badge', containerStatusClass(item)]">
|
||||
{{ item.state?.status || (item.state?.running ? '运行中' : '未知') }}
|
||||
@ -254,12 +262,14 @@
|
||||
</div>
|
||||
<div class="upload-meta">
|
||||
<span>{{ timeAgo(upload.timestamp) }}</span>
|
||||
<span :class="['status-badge', upload.accepted ? 'online' : 'danger']">
|
||||
{{ upload.accepted ? '通过' : '阻断' }}
|
||||
</span>
|
||||
<span v-if="upload.error?.message" class="status-badge danger">{{ upload.error.message }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<span :class="['status-badge', upload.accepted ? 'online' : 'danger']">
|
||||
{{ upload.accepted ? '通过' : '阻断' }}
|
||||
</span>
|
||||
<span v-if="upload.error?.message" class="status-badge danger">{{
|
||||
upload.error.message
|
||||
}}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section v-else-if="activeSection === 'balance'" key="balance" class="panel">
|
||||
@ -269,7 +279,9 @@
|
||||
<p class="muted">手动刷新,查看 Kimi / DeepSeek / Qwen 余额</p>
|
||||
</div>
|
||||
<div class="balance-actions">
|
||||
<span class="muted" v-if="balanceUpdatedAt">上次刷新:{{ timeAgo(balanceUpdatedAt) }}</span>
|
||||
<span class="muted" v-if="balanceUpdatedAt"
|
||||
>上次刷新:{{ timeAgo(balanceUpdatedAt) }}</span
|
||||
>
|
||||
<button type="button" :disabled="balanceLoading" @click="fetchBalance">
|
||||
{{ balanceLoading ? '查询中...' : '手动刷新' }}
|
||||
</button>
|
||||
@ -280,7 +292,12 @@
|
||||
<span>{{ balanceError }}</span>
|
||||
</section>
|
||||
<div class="balance-grid">
|
||||
<div v-for="card in balanceCards" :key="card.key" class="balance-card" :class="{ error: !card.success }">
|
||||
<div
|
||||
v-for="card in balanceCards"
|
||||
:key="card.key"
|
||||
class="balance-card"
|
||||
:class="{ error: !card.success }"
|
||||
>
|
||||
<div class="balance-card-head">
|
||||
<h3>{{ card.label }}</h3>
|
||||
<span v-if="card.success" class="status-badge online">正常</span>
|
||||
@ -295,7 +312,9 @@
|
||||
<li v-if="card.extras.voucher !== undefined">券:{{ card.extras.voucher }}</li>
|
||||
<li v-if="card.extras.cash !== undefined">现金:{{ card.extras.cash }}</li>
|
||||
<li v-if="card.extras.granted !== undefined">赠送:{{ card.extras.granted }}</li>
|
||||
<li v-if="card.extras.topped_up !== undefined">充值:{{ card.extras.topped_up }}</li>
|
||||
<li v-if="card.extras.topped_up !== undefined">
|
||||
充值:{{ card.extras.topped_up }}
|
||||
</li>
|
||||
<li v-if="card.extras.rate">汇率 USD→CNY:{{ card.extras.rate }}</li>
|
||||
</ul>
|
||||
<p v-else class="muted small">{{ card.error || '未知错误' }}</p>
|
||||
@ -341,10 +360,20 @@
|
||||
<h4>{{ code.code }}</h4>
|
||||
<span>{{ inviteStatus(code.remaining) }}</span>
|
||||
<div class="invite-actions">
|
||||
<button type="button" class="mini-btn" :disabled="inviteSubmitting" @click="editInvite(code)">
|
||||
<button
|
||||
type="button"
|
||||
class="mini-btn"
|
||||
:disabled="inviteSubmitting"
|
||||
@click="editInvite(code)"
|
||||
>
|
||||
调整次数
|
||||
</button>
|
||||
<button type="button" class="mini-btn danger" :disabled="inviteSubmitting" @click="removeInvite(code.code)">
|
||||
<button
|
||||
type="button"
|
||||
class="mini-btn danger"
|
||||
:disabled="inviteSubmitting"
|
||||
@click="removeInvite(code.code)"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
@ -371,7 +400,13 @@ type Snapshot = Record<string, any> | null;
|
||||
|
||||
type SectionId = 'overview' | 'usage' | 'users' | 'containers' | 'uploads' | 'balance' | 'invites';
|
||||
|
||||
const { verified: secondaryVerified, loading: secondaryLoading, error: secondaryError, check: checkSecondary, verify: verifySecondary } = useSecondaryPass();
|
||||
const {
|
||||
verified: secondaryVerified,
|
||||
loading: secondaryLoading,
|
||||
error: secondaryError,
|
||||
check: checkSecondary,
|
||||
verify: verifySecondary
|
||||
} = useSecondaryPass();
|
||||
const secondaryPassword = ref('');
|
||||
|
||||
const loading = ref(true);
|
||||
@ -474,8 +509,17 @@ const fetchCsrfToken = async (): Promise<string> => {
|
||||
};
|
||||
|
||||
const parseInviteRemaining = (raw: unknown): number | null => {
|
||||
const text = String(raw ?? '').trim().toLowerCase();
|
||||
if (!text || text === 'unlimited' || text === 'null' || text === 'none' || text === '不限' || text === '无限制') {
|
||||
const text = String(raw ?? '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (
|
||||
!text ||
|
||||
text === 'unlimited' ||
|
||||
text === 'null' ||
|
||||
text === 'none' ||
|
||||
text === '不限' ||
|
||||
text === '无限制'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(text);
|
||||
@ -494,16 +538,18 @@ const createInvite = async () => {
|
||||
inviteSubmitting.value = true;
|
||||
inviteError.value = null;
|
||||
try {
|
||||
const remaining = newInviteUnlimited.value ? null : parseInviteRemaining(newInviteRemaining.value);
|
||||
const remaining = newInviteUnlimited.value
|
||||
? null
|
||||
: parseInviteRemaining(newInviteRemaining.value);
|
||||
const csrfToken = await fetchCsrfToken();
|
||||
const resp = await fetch('/api/admin/invites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ code, remaining }),
|
||||
body: JSON.stringify({ code, remaining })
|
||||
});
|
||||
const payload = await resp.json();
|
||||
if (!resp.ok || !payload.success) {
|
||||
@ -534,10 +580,10 @@ const editInvite = async (item: any) => {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ code: item?.code, remaining }),
|
||||
body: JSON.stringify({ code: item?.code, remaining })
|
||||
});
|
||||
const payload = await resp.json();
|
||||
if (!resp.ok || !payload.success) {
|
||||
@ -562,10 +608,10 @@ const removeInvite = async (code: string) => {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ code }),
|
||||
body: JSON.stringify({ code })
|
||||
});
|
||||
const payload = await resp.json();
|
||||
if (!resp.ok || !payload.success) {
|
||||
@ -693,9 +739,11 @@ const balanceCards = computed(() => {
|
||||
success,
|
||||
currency,
|
||||
amount,
|
||||
amountCny: data.available_cny ?? (currency === 'USD' && usdToCnyRate.value && amount != null
|
||||
? Math.round(amount * usdToCnyRate.value * 100) / 100
|
||||
: null),
|
||||
amountCny:
|
||||
data.available_cny ??
|
||||
(currency === 'USD' && usdToCnyRate.value && amount != null
|
||||
? Math.round(amount * usdToCnyRate.value * 100) / 100
|
||||
: null),
|
||||
extras: data,
|
||||
error: success ? null : data.error
|
||||
};
|
||||
@ -717,7 +765,8 @@ const metricCards = computed(() => [
|
||||
title: '容器使用',
|
||||
value: `${containerSummary.value.active || 0}/${containerSummary.value.max_containers ?? '∞'}`,
|
||||
sub:
|
||||
containerSummary.value.available_slots === null || containerSummary.value.available_slots === undefined
|
||||
containerSummary.value.available_slots === null ||
|
||||
containerSummary.value.available_slots === undefined
|
||||
? '无限制'
|
||||
: `${containerSummary.value.available_slots} 个空闲`
|
||||
},
|
||||
@ -819,7 +868,7 @@ const inviteStatus = (remaining: number | null | undefined) => {
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
background: #f7f3ea;
|
||||
font-family: 'Iowan Old Style', ui-serif, Georgia, Cambria, "Times New Roman", serif;
|
||||
font-family: 'Iowan Old Style', ui-serif, Georgia, Cambria, 'Times New Roman', serif;
|
||||
color: #2a2013;
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -896,7 +945,10 @@ button {
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
box-shadow: inset 0 -2px 0 rgba(189, 93, 58, 0.25);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.2s ease;
|
||||
transition:
|
||||
transform 0.15s ease,
|
||||
box-shadow 0.15s ease,
|
||||
background 0.2s ease;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
|
||||
@ -16,7 +16,14 @@
|
||||
<button type="button" :disabled="secondaryLoading" @click="handleVerifySecondary">
|
||||
{{ secondaryLoading ? '校验中...' : '确认' }}
|
||||
</button>
|
||||
<button type="button" class="ghost-btn" :disabled="secondaryLoading" @click="checkSecondary">重新检测</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ghost-btn"
|
||||
:disabled="secondaryLoading"
|
||||
@click="checkSecondary"
|
||||
>
|
||||
重新检测
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="secondaryError" class="secondary-error">{{ secondaryError }}</p>
|
||||
</div>
|
||||
@ -49,12 +56,17 @@
|
||||
<div class="metric-card">
|
||||
<p>累计 Token</p>
|
||||
<strong>{{ formatNumber(overview.token_totals?.total_tokens || 0) }}</strong>
|
||||
<span class="muted">输入 {{ formatNumber(overview.token_totals?.input_tokens || 0) }} / 输出 {{ formatNumber(overview.token_totals?.output_tokens || 0) }}</span>
|
||||
<span class="muted"
|
||||
>输入 {{ formatNumber(overview.token_totals?.input_tokens || 0) }} / 输出
|
||||
{{ formatNumber(overview.token_totals?.output_tokens || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<p>活动容器</p>
|
||||
<strong>{{ overview.totals?.containers_active || 0 }}</strong>
|
||||
<span class="muted">可用槽位 {{ overview.totals?.available_container_slots ?? '—' }}</span>
|
||||
<span class="muted"
|
||||
>可用槽位 {{ overview.totals?.available_container_slots ?? '—' }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -91,7 +103,9 @@
|
||||
<td colspan="10">暂无 API 账号</td>
|
||||
</tr>
|
||||
<tr v-for="u in users" :key="u.username">
|
||||
<td><strong>{{ u.username }}</strong></td>
|
||||
<td>
|
||||
<strong>{{ u.username }}</strong>
|
||||
</td>
|
||||
<td>{{ u.note || '—' }}</td>
|
||||
<td>{{ formatTime(u.created_at) }}</td>
|
||||
<td>{{ formatNumber(u.usage?.total_requests || 0) }}</td>
|
||||
@ -104,9 +118,17 @@
|
||||
</td>
|
||||
<td class="token-cell">
|
||||
<span class="token-text">
|
||||
{{ revealTokens[u.username]?.visible ? (revealTokens[u.username]?.token || '—') : '••••••••••' }}
|
||||
{{
|
||||
revealTokens[u.username]?.visible
|
||||
? revealTokens[u.username]?.token || '—'
|
||||
: '••••••••••'
|
||||
}}
|
||||
</span>
|
||||
<button class="icon-btn" @click="toggleReveal(u.username)" :disabled="revealLoading === u.username">
|
||||
<button
|
||||
class="icon-btn"
|
||||
@click="toggleReveal(u.username)"
|
||||
:disabled="revealLoading === u.username"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path
|
||||
d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12Zm11 3a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"
|
||||
@ -116,12 +138,22 @@
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="icon-btn" @click="copyToken(u.username)" :disabled="revealLoading === u.username">
|
||||
<button
|
||||
class="icon-btn"
|
||||
@click="copyToken(u.username)"
|
||||
:disabled="revealLoading === u.username"
|
||||
>
|
||||
📋
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<button class="link danger" @click="deleteUser(u.username)" :disabled="deleting === u.username">删除</button>
|
||||
<button
|
||||
class="link danger"
|
||||
@click="deleteUser(u.username)"
|
||||
:disabled="deleting === u.username"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -157,7 +189,10 @@
|
||||
<td>{{ formatPercentNumber(c.stats?.cpu_percent) }}</td>
|
||||
<td>{{ formatPercentNumber(c.stats?.memory?.percent) }}</td>
|
||||
<td>{{ formatBytes(c.stats?.memory?.used_bytes) }}</td>
|
||||
<td>{{ formatBytes(c.stats?.net_io?.rx_bytes) }} / {{ formatBytes(c.stats?.net_io?.tx_bytes) }}</td>
|
||||
<td>
|
||||
{{ formatBytes(c.stats?.net_io?.rx_bytes) }} /
|
||||
{{ formatBytes(c.stats?.net_io?.tx_bytes) }}
|
||||
</td>
|
||||
<td>
|
||||
<span :class="['status-badge', c.running ? 'online' : 'offline']">
|
||||
{{ c.running ? '运行中' : '未运行' }}
|
||||
@ -179,7 +214,8 @@
|
||||
<div>
|
||||
<strong>{{ item.original_name || '未命名文件' }}</strong>
|
||||
<div class="muted small">
|
||||
用户 {{ item.username }} · {{ formatBytes(item.size) }} · {{ item.source || 'unknown' }}
|
||||
用户 {{ item.username }} · {{ formatBytes(item.size) }} ·
|
||||
{{ item.source || 'unknown' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="muted small">
|
||||
@ -204,7 +240,12 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="balance-grid">
|
||||
<div v-for="card in balanceCards" :key="card.key" class="balance-card" :class="{ error: !card.success }">
|
||||
<div
|
||||
v-for="card in balanceCards"
|
||||
:key="card.key"
|
||||
class="balance-card"
|
||||
:class="{ error: !card.success }"
|
||||
>
|
||||
<div class="balance-card-head">
|
||||
<h4>{{ card.label }}</h4>
|
||||
<span :class="['status-badge', card.success ? 'online' : 'danger']">
|
||||
@ -224,7 +265,13 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useSecondaryPass } from './useSecondaryPass';
|
||||
|
||||
const { verified: secondaryVerified, loading: secondaryLoading, error: secondaryError, check: checkSecondary, verify: verifySecondary } = useSecondaryPass();
|
||||
const {
|
||||
verified: secondaryVerified,
|
||||
loading: secondaryLoading,
|
||||
error: secondaryError,
|
||||
check: checkSecondary,
|
||||
verify: verifySecondary
|
||||
} = useSecondaryPass();
|
||||
const secondaryPassword = ref('');
|
||||
|
||||
const loading = ref(false);
|
||||
@ -250,7 +297,10 @@ const containerItems = computed(() => {
|
||||
return Object.keys(items).map((key) => ({ key, ...(items[key] || {}) }));
|
||||
});
|
||||
const totalRequests = computed(() =>
|
||||
users.value.reduce((sum, u) => sum + ((u.usage && u.usage.total_requests) ? u.usage.total_requests : 0), 0)
|
||||
users.value.reduce(
|
||||
(sum, u) => sum + (u.usage && u.usage.total_requests ? u.usage.total_requests : 0),
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
const balanceCards = computed(() => {
|
||||
@ -264,7 +314,7 @@ const balanceCards = computed(() => {
|
||||
amount: data.available ?? data.total ?? null,
|
||||
amountCny: data.amount_cny ?? data.amountCny ?? null,
|
||||
currency: data.currency || '',
|
||||
error: data.error,
|
||||
error: data.error
|
||||
};
|
||||
});
|
||||
});
|
||||
@ -339,7 +389,7 @@ const fetchUsers = async () => {
|
||||
...u,
|
||||
usage: u.usage || { total_requests: 0, endpoints: {}, last_request_at: null },
|
||||
tokens: u.tokens || { total_tokens: 0, input_tokens: 0, output_tokens: 0 },
|
||||
storage: u.storage || { total_bytes: 0 },
|
||||
storage: u.storage || { total_bytes: 0 }
|
||||
}));
|
||||
users.value.forEach((u) => {
|
||||
map[u.username] = revealTokens.value[u.username] || { token: '', visible: false };
|
||||
@ -383,7 +433,7 @@ const createUser = async () => {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, note: createForm.value.note }),
|
||||
body: JSON.stringify({ username, note: createForm.value.note })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok || !data.success) throw new Error(data.error || '创建失败');
|
||||
@ -404,7 +454,7 @@ const deleteUser = async (username: string) => {
|
||||
try {
|
||||
const resp = await fetch(`/api/admin/api-users/${encodeURIComponent(username)}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok || !data.success) throw new Error(data.error || '删除失败');
|
||||
@ -425,7 +475,7 @@ const toggleReveal = async (username: string) => {
|
||||
revealLoading.value = username;
|
||||
try {
|
||||
const resp = await fetch(`/api/admin/api-users/${encodeURIComponent(username)}/token`, {
|
||||
credentials: 'same-origin',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok || !data.success) throw new Error(data.error || '获取失败');
|
||||
@ -503,7 +553,7 @@ onBeforeUnmount(() => {
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
background: #f7f3ea;
|
||||
font-family: 'Iowan Old Style', ui-serif, Georgia, Cambria, "Times New Roman", serif;
|
||||
font-family: 'Iowan Old Style', ui-serif, Georgia, Cambria, 'Times New Roman', serif;
|
||||
color: #2a2013;
|
||||
}
|
||||
|
||||
@ -549,11 +599,11 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: rgba(255,255,255,0.94);
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
border: 1px solid rgba(44,32,19,0.12);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.6);
|
||||
border: 1px solid rgba(44, 32, 19, 0.12);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.metric-card p {
|
||||
@ -594,7 +644,7 @@ onBeforeUnmount(() => {
|
||||
.create-form input {
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0,0,0,0.12);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
button {
|
||||
@ -626,9 +676,10 @@ table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
th,
|
||||
td {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.08);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
@ -640,8 +691,8 @@ th, td {
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: rgba(255,255,255,0.9);
|
||||
border: 1px solid rgba(0,0,0,0.12);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
@ -667,7 +718,7 @@ th, td {
|
||||
}
|
||||
|
||||
.status-badge.offline {
|
||||
background: rgba(0,0,0,0.06);
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #5f5140;
|
||||
}
|
||||
|
||||
@ -693,8 +744,8 @@ th, td {
|
||||
.upload-item {
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255,255,255,0.92);
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.grid {
|
||||
@ -718,8 +769,8 @@ th, td {
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
background: rgba(255,255,255,0.92);
|
||||
border: 1px solid rgba(44,32,19,0.12);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(44, 32, 19, 0.12);
|
||||
border-radius: 14px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
@ -13,10 +13,17 @@
|
||||
@keyup.enter="handleVerifySecondary"
|
||||
/>
|
||||
<div class="secondary-actions">
|
||||
<button type="button" class="primary" :disabled="secondaryLoading" @click="handleVerifySecondary">
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
:disabled="secondaryLoading"
|
||||
@click="handleVerifySecondary"
|
||||
>
|
||||
{{ secondaryLoading ? '校验中...' : '确认' }}
|
||||
</button>
|
||||
<button type="button" class="ghost" :disabled="secondaryLoading" @click="checkSecondary">重新检测</button>
|
||||
<button type="button" class="ghost" :disabled="secondaryLoading" @click="checkSecondary">
|
||||
重新检测
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="secondaryError" class="secondary-error">{{ secondaryError }}</p>
|
||||
</div>
|
||||
@ -29,12 +36,16 @@
|
||||
<p>每个工具一个文件夹,三层文件独立存放;仅管理员可见并可调用。</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="button" class="primary" @click="openCreateModal">新建工具</button>
|
||||
<button type="button" class="ghost" @click="refresh" :disabled="loading">{{ loading ? '刷新中...' : '刷新列表' }}</button>
|
||||
<a class="ghost" href="/static/custom_tools/guide.html" target="_blank" rel="noopener">查看开发指南</a>
|
||||
<button type="button" class="primary" @click="openCreateModal">新建工具</button>
|
||||
<button type="button" class="ghost" @click="refresh" :disabled="loading">
|
||||
{{ loading ? '刷新中...' : '刷新列表' }}
|
||||
</button>
|
||||
<a class="ghost" href="/static/custom_tools/guide.html" target="_blank" rel="noopener"
|
||||
>查看开发指南</a
|
||||
>
|
||||
<a class="ghost" href="/admin/monitor" target="_blank" rel="noopener">返回监控</a>
|
||||
<a class="ghost" href="/admin/policy" target="_blank" rel="noopener">策略配置</a>
|
||||
</div>
|
||||
<a class="ghost" href="/admin/policy" target="_blank" rel="noopener">策略配置</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="panel" v-if="error">
|
||||
@ -84,28 +95,49 @@
|
||||
<p>{{ activeTool.description || '未填写描述' }}</p>
|
||||
</div>
|
||||
<div class="drawer-actions">
|
||||
<button type="button" class="primary" @click="saveAll" :disabled="saving">{{ saving ? '保存中...' : '保存' }}</button>
|
||||
<button type="button" class="primary" @click="saveAll" :disabled="saving">
|
||||
{{ saving ? '保存中...' : '保存' }}
|
||||
</button>
|
||||
<button type="button" class="danger" @click="openDeleteConfirm()">删除</button>
|
||||
<button type="button" class="ghost" @click="closeEditor">关闭</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="tabs">
|
||||
<button :class="{active: tab==='definition'}" @click="tab='definition'">definition.json</button>
|
||||
<button :class="{active: tab==='execution'}" @click="tab='execution'">execution.py</button>
|
||||
<button :class="{active: tab==='return'}" @click="tab='return'">return.json</button>
|
||||
<button :class="{active: tab==='meta'}" @click="tab='meta'">meta.json</button>
|
||||
<button :class="{ active: tab === 'definition' }" @click="tab = 'definition'">
|
||||
definition.json
|
||||
</button>
|
||||
<button :class="{ active: tab === 'execution' }" @click="tab = 'execution'">
|
||||
execution.py
|
||||
</button>
|
||||
<button :class="{ active: tab === 'return' }" @click="tab = 'return'">return.json</button>
|
||||
<button :class="{ active: tab === 'meta' }" @click="tab = 'meta'">meta.json</button>
|
||||
</div>
|
||||
|
||||
<div class="editor">
|
||||
<textarea v-if="tab==='definition'" v-model="buffers.definition" spellcheck="false"></textarea>
|
||||
<textarea v-else-if="tab==='execution'" v-model="buffers.execution" spellcheck="false"></textarea>
|
||||
<textarea v-else-if="tab==='return'" v-model="buffers.return" spellcheck="false"></textarea>
|
||||
<textarea
|
||||
v-if="tab === 'definition'"
|
||||
v-model="buffers.definition"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
<textarea
|
||||
v-else-if="tab === 'execution'"
|
||||
v-model="buffers.execution"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
<textarea
|
||||
v-else-if="tab === 'return'"
|
||||
v-model="buffers.return"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
<textarea v-else v-model="buffers.meta" spellcheck="false"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<p>提示:execution.py 中的字典/集合需要用 <code v-pre>{{ ... }}</code> 包裹,避免被模板替换。</p>
|
||||
<p>
|
||||
提示:execution.py 中的字典/集合需要用
|
||||
<code v-pre>{{ ... }}</code> 包裹,避免被模板替换。
|
||||
</p>
|
||||
<p>保存后无需重启,系统会自动 reload 自定义工具。</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -121,8 +153,10 @@
|
||||
<label>工具 ID(小写/下划线):<input v-model="createForm.id" /></label>
|
||||
<label>描述:<input v-model="createForm.description" /></label>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="ghost" @click="createModal=false">取消</button>
|
||||
<button type="button" class="primary" @click="createTool" :disabled="creating">{{ creating ? '创建中...' : '创建' }}</button>
|
||||
<button type="button" class="ghost" @click="createModal = false">取消</button>
|
||||
<button type="button" class="primary" @click="createTool" :disabled="creating">
|
||||
{{ creating ? '创建中...' : '创建' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -133,10 +167,14 @@
|
||||
<div v-if="confirmDeleteModal" class="modal-backdrop">
|
||||
<div class="modal">
|
||||
<h3>确认删除</h3>
|
||||
<p class="delete-tip">确定删除工具 <strong>{{ deleteTargetId }}</strong> 吗?该操作不可恢复。</p>
|
||||
<p class="delete-tip">
|
||||
确定删除工具 <strong>{{ deleteTargetId }}</strong> 吗?该操作不可恢复。
|
||||
</p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="ghost" @click="confirmDeleteModal=false">取消</button>
|
||||
<button type="button" class="danger" @click="performDelete" :disabled="deleting">{{ deleting ? '删除中...' : '确认删除' }}</button>
|
||||
<button type="button" class="ghost" @click="confirmDeleteModal = false">取消</button>
|
||||
<button type="button" class="danger" @click="performDelete" :disabled="deleting">
|
||||
{{ deleting ? '删除中...' : '确认删除' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -159,7 +197,13 @@ interface CustomTool {
|
||||
return_file?: string;
|
||||
}
|
||||
|
||||
const { verified: secondaryVerified, loading: secondaryLoading, error: secondaryError, check: checkSecondary, verify: verifySecondary } = useSecondaryPass();
|
||||
const {
|
||||
verified: secondaryVerified,
|
||||
loading: secondaryLoading,
|
||||
error: secondaryError,
|
||||
check: checkSecondary,
|
||||
verify: verifySecondary
|
||||
} = useSecondaryPass();
|
||||
const secondaryPassword = ref('');
|
||||
|
||||
const tools = ref<CustomTool[]>([]);
|
||||
@ -221,10 +265,20 @@ const openEditorPage = (tool: CustomTool) => {
|
||||
const loadBuffers = async (id: string) => {
|
||||
if (!secondaryVerified.value) return;
|
||||
try {
|
||||
buffers.definition = await fetchText(`/api/admin/custom-tools/file?id=${encodeURIComponent(id)}&name=definition.json`);
|
||||
buffers.execution = await fetchText(`/api/admin/custom-tools/file?id=${encodeURIComponent(id)}&name=execution.py`);
|
||||
buffers.return = await fetchText(`/api/admin/custom-tools/file?id=${encodeURIComponent(id)}&name=return.json`, '');
|
||||
buffers.meta = await fetchText(`/api/admin/custom-tools/file?id=${encodeURIComponent(id)}&name=meta.json`, '');
|
||||
buffers.definition = await fetchText(
|
||||
`/api/admin/custom-tools/file?id=${encodeURIComponent(id)}&name=definition.json`
|
||||
);
|
||||
buffers.execution = await fetchText(
|
||||
`/api/admin/custom-tools/file?id=${encodeURIComponent(id)}&name=execution.py`
|
||||
);
|
||||
buffers.return = await fetchText(
|
||||
`/api/admin/custom-tools/file?id=${encodeURIComponent(id)}&name=return.json`,
|
||||
''
|
||||
);
|
||||
buffers.meta = await fetchText(
|
||||
`/api/admin/custom-tools/file?id=${encodeURIComponent(id)}&name=meta.json`,
|
||||
''
|
||||
);
|
||||
} catch (e: any) {
|
||||
error.value = e.message;
|
||||
}
|
||||
@ -270,11 +324,6 @@ const reloadRegistry = async () => {
|
||||
await fetch('/api/admin/custom-tools/reload', { method: 'POST', credentials: 'same-origin' });
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (!activeTool.value) return;
|
||||
openDeleteConfirm(activeTool.value);
|
||||
};
|
||||
|
||||
const openDeleteConfirm = (tool?: CustomTool) => {
|
||||
const target = tool || activeTool.value;
|
||||
if (!target) return;
|
||||
@ -287,7 +336,10 @@ const performDelete = async () => {
|
||||
if (!deleteTargetId.value) return;
|
||||
deleting.value = true;
|
||||
try {
|
||||
const res = await fetch(`/api/admin/custom-tools?id=${encodeURIComponent(deleteTargetId.value)}`, { method: 'DELETE', credentials: 'same-origin' });
|
||||
const res = await fetch(
|
||||
`/api/admin/custom-tools?id=${encodeURIComponent(deleteTargetId.value)}`,
|
||||
{ method: 'DELETE', credentials: 'same-origin' }
|
||||
);
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.error || '删除失败');
|
||||
activeTool.value = null;
|
||||
@ -387,9 +439,9 @@ watch(secondaryVerified, async (val) => {
|
||||
/* Claude 经典:米色+白色+深棕的柔和高对比配色 */
|
||||
:global(html, body) {
|
||||
min-height: 100%;
|
||||
background: radial-gradient(140% 120% at 20% 20%, rgba(239, 229, 214, 0.9), transparent),
|
||||
radial-gradient(120% 120% at 80% 0%, rgba(255, 255, 255, 0.9), transparent),
|
||||
#f8f3e8;
|
||||
background:
|
||||
radial-gradient(140% 120% at 20% 20%, rgba(239, 229, 214, 0.9), transparent),
|
||||
radial-gradient(120% 120% at 80% 0%, rgba(255, 255, 255, 0.9), transparent), #f8f3e8;
|
||||
}
|
||||
:global(#custom-tools-app) {
|
||||
min-height: 100vh;
|
||||
@ -411,7 +463,15 @@ watch(secondaryVerified, async (val) => {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 28px;
|
||||
font-family: 'Space Grotesk', 'Noto Sans SC', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-family:
|
||||
'Space Grotesk',
|
||||
'Noto Sans SC',
|
||||
'Inter',
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
sans-serif;
|
||||
color: var(--ink);
|
||||
background: transparent;
|
||||
}
|
||||
@ -465,7 +525,11 @@ watch(secondaryVerified, async (val) => {
|
||||
border-radius: var(--radius-md);
|
||||
padding: 14px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, box-shadow 0.2s, transform 0.1s, background 0.2s;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
box-shadow 0.2s,
|
||||
transform 0.1s,
|
||||
background 0.2s;
|
||||
background: linear-gradient(180deg, #fffdf8 0%, #ffffff 50%, #fbf6ed 100%);
|
||||
}
|
||||
.tool-card:hover {
|
||||
@ -643,8 +707,13 @@ watch(secondaryVerified, async (val) => {
|
||||
margin: 4px 0 8px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.error { color: #dc2626; }
|
||||
.empty { color: var(--muted); padding: 12px; }
|
||||
.error {
|
||||
color: #dc2626;
|
||||
}
|
||||
.empty {
|
||||
color: var(--muted);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* 按钮样式与主站一致风格 */
|
||||
.primary,
|
||||
@ -658,7 +727,11 @@ watch(secondaryVerified, async (val) => {
|
||||
padding: 10px 18px;
|
||||
}
|
||||
.primary {
|
||||
background: linear-gradient(135deg, var(--claude-accent, #da7756) 0%, var(--claude-accent-strong, #bd5d3a) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--claude-accent, #da7756) 0%,
|
||||
var(--claude-accent-strong, #bd5d3a) 100%
|
||||
);
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
@ -42,9 +42,9 @@ onMounted(() => {
|
||||
<style scoped>
|
||||
:global(html, body) {
|
||||
min-height: 100%;
|
||||
background: radial-gradient(140% 120% at 20% 20%, rgba(239, 229, 214, 0.9), transparent),
|
||||
radial-gradient(120% 120% at 80% 0%, rgba(255, 255, 255, 0.9), transparent),
|
||||
#f8f3e8;
|
||||
background:
|
||||
radial-gradient(140% 120% at 20% 20%, rgba(239, 229, 214, 0.9), transparent),
|
||||
radial-gradient(120% 120% at 80% 0%, rgba(255, 255, 255, 0.9), transparent), #f8f3e8;
|
||||
}
|
||||
:global(#custom-tools-guide) {
|
||||
min-height: 100vh;
|
||||
@ -61,7 +61,15 @@ onMounted(() => {
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px 32px;
|
||||
font-family: 'Space Grotesk', 'Noto Sans SC', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-family:
|
||||
'Space Grotesk',
|
||||
'Noto Sans SC',
|
||||
'Inter',
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
sans-serif;
|
||||
color: var(--ink);
|
||||
}
|
||||
.guide-header {
|
||||
|
||||
@ -13,10 +13,22 @@
|
||||
@keyup.enter="handleVerifySecondary"
|
||||
/>
|
||||
<div class="secondary-actions">
|
||||
<button type="button" class="primary" :disabled="secondaryLoading" @click="handleVerifySecondary">
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
:disabled="secondaryLoading"
|
||||
@click="handleVerifySecondary"
|
||||
>
|
||||
{{ secondaryLoading ? '校验中...' : '确认' }}
|
||||
</button>
|
||||
<button type="button" class="ghost-btn" :disabled="secondaryLoading" @click="checkSecondary">重新检测</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ghost-btn"
|
||||
:disabled="secondaryLoading"
|
||||
@click="checkSecondary"
|
||||
>
|
||||
重新检测
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="secondaryError" class="secondary-error">{{ secondaryError }}</p>
|
||||
</div>
|
||||
@ -33,7 +45,12 @@
|
||||
<span class="caret">▾</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" v-if="targetMenuOpen">
|
||||
<button v-for="opt in targetOptions" :key="opt.value" type="button" @click="pickTargetType(opt.value)">
|
||||
<button
|
||||
v-for="opt in targetOptions"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
@click="pickTargetType(opt.value)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
@ -89,7 +106,7 @@
|
||||
<div class="tool-select-search">
|
||||
<input v-model="toolSearch" type="text" placeholder="搜索或添加工具 ID" />
|
||||
</div>
|
||||
<div class="tool-select-options">
|
||||
<div class="tool-select-options">
|
||||
<label v-for="tool in filteredToolOptions(cat)" :key="tool">
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -111,7 +128,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<label class="toggle-row compact">
|
||||
<input type="checkbox" :checked="getCategoryDefault(cat.id)" @change="setCategoryDefault(cat.id, $event.target.checked)" />
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="getCategoryDefault(cat.id)"
|
||||
@change="setCategoryDefault(cat.id, $event.target.checked)"
|
||||
/>
|
||||
<span class="fancy-check" aria-hidden="true">
|
||||
<svg viewBox="0 0 64 64">
|
||||
<path
|
||||
@ -145,11 +166,7 @@
|
||||
<h2>模型禁用</h2>
|
||||
</div>
|
||||
<div class="toggle-grid">
|
||||
<label
|
||||
v-for="model in defaults.models"
|
||||
:key="model"
|
||||
class="toggle-row"
|
||||
>
|
||||
<label v-for="model in defaults.models" :key="model" class="toggle-row">
|
||||
<input type="checkbox" :checked="isModelDisabled(model)" @change="toggleModel(model)" />
|
||||
<span class="fancy-check" aria-hidden="true">
|
||||
<svg viewBox="0 0 64 64">
|
||||
@ -169,12 +186,12 @@
|
||||
<h2>前端禁用项</h2>
|
||||
</div>
|
||||
<div class="toggle-grid">
|
||||
<label
|
||||
v-for="key in defaults.ui_block_keys"
|
||||
:key="key"
|
||||
class="toggle-row"
|
||||
>
|
||||
<input type="checkbox" :checked="!!form.config.ui_blocks[key]" @change="toggleUiBlock(key, $event)" />
|
||||
<label v-for="key in defaults.ui_block_keys" :key="key" class="toggle-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="!!form.config.ui_blocks[key]"
|
||||
@change="toggleUiBlock(key, $event)"
|
||||
/>
|
||||
<span class="fancy-check" aria-hidden="true">
|
||||
<svg viewBox="0 0 64 64">
|
||||
<path
|
||||
@ -242,7 +259,13 @@ const defaults = reactive({
|
||||
ui_block_keys: [] as string[]
|
||||
});
|
||||
|
||||
const { verified: secondaryVerified, loading: secondaryLoading, error: secondaryError, check: checkSecondary, verify: verifySecondary } = useSecondaryPass();
|
||||
const {
|
||||
verified: secondaryVerified,
|
||||
loading: secondaryLoading,
|
||||
error: secondaryError,
|
||||
check: checkSecondary,
|
||||
verify: verifySecondary
|
||||
} = useSecondaryPass();
|
||||
const secondaryPassword = ref('');
|
||||
|
||||
const form = reactive({
|
||||
@ -275,7 +298,9 @@ const targetOptions = [
|
||||
{ value: 'invite', label: '邀请码' }
|
||||
] as const;
|
||||
|
||||
const targetTypeLabel = computed(() => targetOptions.find(o => o.value === form.target_type)?.label || '全局');
|
||||
const targetTypeLabel = computed(
|
||||
() => targetOptions.find((o) => o.value === form.target_type)?.label || '全局'
|
||||
);
|
||||
const targetMenuOpen = ref(false);
|
||||
const openForceMenu = ref<string | null>(null);
|
||||
const openToolMenu = ref<string | null>(null);
|
||||
@ -497,9 +522,7 @@ function rebuildCategoryOverrides() {
|
||||
if (!item.id) return;
|
||||
map[item.id] = {
|
||||
label: item.label || item.id,
|
||||
tools: (item.tools || [])
|
||||
.map((s) => (typeof s === 'string' ? s.trim() : ''))
|
||||
.filter(Boolean),
|
||||
tools: (item.tools || []).map((s) => (typeof s === 'string' ? s.trim() : '')).filter(Boolean),
|
||||
default_enabled: !!item.default_enabled
|
||||
};
|
||||
if (item.forced === true || item.forced === false) {
|
||||
@ -642,7 +665,7 @@ onBeforeUnmount(() => {
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
background: #f7f3ea;
|
||||
font-family: 'Iowan Old Style', ui-serif, Georgia, Cambria, "Times New Roman", serif;
|
||||
font-family: 'Iowan Old Style', ui-serif, Georgia, Cambria, 'Times New Roman', serif;
|
||||
color: #2a2013;
|
||||
}
|
||||
|
||||
@ -680,7 +703,7 @@ onBeforeUnmount(() => {
|
||||
.ghost-btn {
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0,0,0,0.12);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #2a2013;
|
||||
display: inline-flex;
|
||||
@ -700,9 +723,9 @@ onBeforeUnmount(() => {
|
||||
left: 0;
|
||||
min-width: 160px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0,0,0,0.12);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 12px 30px rgba(0,0,0,0.12);
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@ -719,13 +742,13 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.dropdown-menu button:hover {
|
||||
background: rgba(0,0,0,0.04);
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.text-field input {
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0,0,0,0.12);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@ -737,7 +760,7 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.banner.success {
|
||||
@ -796,12 +819,14 @@ onBeforeUnmount(() => {
|
||||
.category-row select {
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0,0,0,0.12);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.id-input {
|
||||
font-family: "SFMono-Regular", ui-monospace, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-family:
|
||||
'SFMono-Regular', ui-monospace, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
.checkbox-cell {
|
||||
@ -834,8 +859,8 @@ onBeforeUnmount(() => {
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(0,0,0,0.12);
|
||||
background: rgba(255,255,255,0.92);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
@ -848,7 +873,7 @@ onBeforeUnmount(() => {
|
||||
width: 34px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
background: rgba(0,0,0,0.12);
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
@ -862,7 +887,7 @@ onBeforeUnmount(() => {
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
@ -916,7 +941,9 @@ onBeforeUnmount(() => {
|
||||
cursor: pointer;
|
||||
padding: 6px 10px;
|
||||
border-radius: 10px;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.link.danger {
|
||||
@ -933,7 +960,7 @@ onBeforeUnmount(() => {
|
||||
button {
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0,0,0,0.12);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -960,7 +987,10 @@ button:disabled {
|
||||
border: 1px solid var(--theme-control-border, rgba(118, 103, 84, 0.25));
|
||||
background: var(--theme-surface-muted, rgba(255, 255, 255, 0.85));
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
background 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-row.compact {
|
||||
@ -1001,7 +1031,10 @@ button:disabled {
|
||||
stroke-width: 5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
transition: stroke-dasharray 0.5s ease, stroke-dashoffset 0.5s ease, stroke 0.2s ease;
|
||||
transition:
|
||||
stroke-dasharray 0.5s ease,
|
||||
stroke-dashoffset 0.5s ease,
|
||||
stroke 0.2s ease;
|
||||
stroke-dasharray: 241 9999999;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
@ -1029,7 +1062,7 @@ button:disabled {
|
||||
gap: 8px;
|
||||
padding: 9px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0,0,0,0.12);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
min-height: 40px;
|
||||
@ -1037,7 +1070,7 @@ button:disabled {
|
||||
|
||||
.tool-select.open .tool-select-trigger {
|
||||
border-color: rgba(118, 103, 84, 0.4);
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,0.08);
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.tool-badges {
|
||||
@ -1068,7 +1101,7 @@ button:disabled {
|
||||
background: #fffaf4;
|
||||
border: 1px solid rgba(118, 103, 84, 0.2);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 16px 36px rgba(0,0,0,0.12);
|
||||
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.12);
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -1080,7 +1113,7 @@ button:disabled {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0,0,0,0.12);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@ -1104,8 +1137,8 @@ button:disabled {
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.85);
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ export function useSecondaryPass() {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ password }),
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok || !data.success) {
|
||||
@ -48,7 +48,7 @@ export function useSecondaryPass() {
|
||||
loading,
|
||||
error,
|
||||
check,
|
||||
verify,
|
||||
verify
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -19,12 +19,20 @@
|
||||
<div v-if="msg.content" class="bubble-text">{{ msg.content }}</div>
|
||||
<div v-if="msg.images && msg.images.length" class="image-inline-row">
|
||||
<div class="image-thumbnail-wrapper" v-for="img in msg.images" :key="img">
|
||||
<img :src="getPreviewUrl(img)" :alt="formatImageName(img)" class="image-thumbnail" />
|
||||
<img
|
||||
:src="getPreviewUrl(img)"
|
||||
:alt="formatImageName(img)"
|
||||
class="image-thumbnail"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="msg.videos && msg.videos.length" class="image-inline-row video-inline-row">
|
||||
<div class="image-thumbnail-wrapper" v-for="video in msg.videos" :key="video">
|
||||
<img :src="getPreviewUrl(video)" :alt="formatImageName(video)" class="image-thumbnail" />
|
||||
<img
|
||||
:src="getPreviewUrl(video)"
|
||||
:alt="formatImageName(video)"
|
||||
class="image-thumbnail"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -32,10 +40,19 @@
|
||||
<div v-else-if="msg.role === 'assistant'" class="assistant-message">
|
||||
<!-- 在 assistant 消息前显示 AI Assistant 头部 -->
|
||||
<!-- 只有当前一条消息是 user 且当前消息有内容时才显示 -->
|
||||
<div v-if="index > 0 && filteredMessages[index - 1].role === 'user' && (hasRenderableAssistantActions(msg.actions || []) || msg.awaitingFirstContent)" class="message-header icon-label">
|
||||
<div
|
||||
v-if="
|
||||
index > 0 &&
|
||||
filteredMessages[index - 1].role === 'user' &&
|
||||
(hasRenderableAssistantActions(msg.actions || []) || msg.awaitingFirstContent)
|
||||
"
|
||||
class="message-header icon-label"
|
||||
>
|
||||
<span class="icon icon-sm" :style="iconStyleSafe('bot')" aria-hidden="true"></span>
|
||||
<span>{{ aiAssistantName }}</span>
|
||||
<span v-if="assistantWorkLabel(index)" class="assistant-work-status">{{ assistantWorkLabel(index) }}</span>
|
||||
<span v-if="assistantWorkLabel(index)" class="assistant-work-status">{{
|
||||
assistantWorkLabel(index)
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="msg.awaitingFirstContent"
|
||||
@ -77,7 +94,7 @@
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="stackedBlocksEnabled">
|
||||
<template v-for="(group, groupIndex) in splitActionGroups(msg.actions || [], index)" :key="group.key">
|
||||
<template v-for="group in splitActionGroups(msg.actions || [], index)" :key="group.key">
|
||||
<StackedBlocks
|
||||
v-if="group.kind === 'stack'"
|
||||
class="stacked-blocks-wrapper"
|
||||
@ -103,53 +120,90 @@
|
||||
'streaming-content': group.action?.streaming,
|
||||
'completed-tool': group.action?.type === 'tool' && !group.action?.streaming,
|
||||
'immediate-show':
|
||||
group.action?.streaming || group.action?.type === 'text' || group.action?.type === 'thinking',
|
||||
group.action?.streaming ||
|
||||
group.action?.type === 'text' ||
|
||||
group.action?.type === 'thinking',
|
||||
'thinking-finished': group.action?.type === 'thinking' && !group.action?.streaming
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="group.action?.type === 'thinking'"
|
||||
class="collapsible-block thinking-block"
|
||||
:class="{ expanded: expandedBlocks?.has(group.action.blockId || `${index}-thinking-${group.actionIndex}`) }"
|
||||
:class="{
|
||||
expanded: expandedBlocks?.has(
|
||||
group.action.blockId || `${index}-thinking-${group.actionIndex}`
|
||||
)
|
||||
}"
|
||||
>
|
||||
<div class="collapsible-header" @click="toggleBlock(group.action.blockId || `${index}-thinking-${group.actionIndex}`)">
|
||||
<div
|
||||
class="collapsible-header"
|
||||
@click="
|
||||
toggleBlock(group.action.blockId || `${index}-thinking-${group.actionIndex}`)
|
||||
"
|
||||
>
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<span class="thinking-icon" :class="{ 'thinking-animation': group.action.streaming }">
|
||||
<span class="icon icon-sm" :style="iconStyleSafe('brain')" aria-hidden="true"></span>
|
||||
<span
|
||||
class="thinking-icon"
|
||||
:class="{ 'thinking-animation': group.action.streaming }"
|
||||
>
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="iconStyleSafe('brain')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="status-text">{{ group.action.streaming ? '正在思考...' : '思考过程' }}</span>
|
||||
<span class="status-text">{{
|
||||
group.action.streaming ? '正在思考...' : '思考过程'
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
class="collapsible-content"
|
||||
:ref="el => registerCollapseContent(group.action.blockId || `${index}-thinking-${group.actionIndex}`, el)"
|
||||
:ref="
|
||||
(el) =>
|
||||
registerCollapseContent(
|
||||
group.action.blockId || `${index}-thinking-${group.actionIndex}`,
|
||||
el
|
||||
)
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="content-inner thinking-content"
|
||||
:ref="el => registerThinkingRef(group.action.blockId || `${index}-thinking-${group.actionIndex}`, el)"
|
||||
:ref="
|
||||
(el) =>
|
||||
registerThinkingRef(
|
||||
group.action.blockId || `${index}-thinking-${group.actionIndex}`,
|
||||
el
|
||||
)
|
||||
"
|
||||
@scroll="
|
||||
handleThinkingScroll(
|
||||
group.action.blockId || `${index}-thinking-${group.actionIndex}`,
|
||||
$event
|
||||
)
|
||||
"
|
||||
style="max-height: 240px; overflow-y: auto;"
|
||||
>
|
||||
{{ group.action.content }}
|
||||
style="max-height: 240px; overflow-y: auto"
|
||||
>
|
||||
{{ group.action.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="group.action?.type === 'text'" class="text-output">
|
||||
<div class="text-content" :class="{ 'streaming-text': group.action.streaming }">
|
||||
<div v-if="group.action.streaming" v-html="renderMarkdown(group.action.content, true)"></div>
|
||||
<div
|
||||
v-if="group.action.streaming"
|
||||
v-html="renderMarkdown(group.action.content, true)"
|
||||
></div>
|
||||
<div v-else v-html="renderMarkdown(group.action.content, false)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="group.action?.type === 'system' && getSubAgentSystemNoticeLabel(group.action)"
|
||||
v-else-if="
|
||||
group.action?.type === 'system' && getSubAgentSystemNoticeLabel(group.action)
|
||||
"
|
||||
class="sub-agent-system-summary-line"
|
||||
>
|
||||
{{ getSubAgentSystemNoticeLabel(group.action) }}
|
||||
@ -163,26 +217,56 @@
|
||||
<div class="append-placeholder-content">
|
||||
<template v-if="group.action.append?.success !== false">
|
||||
<div class="icon-label append-status">
|
||||
<span class="icon icon-sm" :style="iconStyleSafe('pencil')" aria-hidden="true"></span>
|
||||
<span>已写入 {{ group.action.append?.path || '目标文件' }} 的追加内容(内容已保存至文件)</span>
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="iconStyleSafe('pencil')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span
|
||||
>已写入
|
||||
{{ group.action.append?.path || '目标文件' }}
|
||||
的追加内容(内容已保存至文件)</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="icon-label append-status append-error-text">
|
||||
<span class="icon icon-sm" :style="iconStyleSafe('x')" aria-hidden="true"></span>
|
||||
<span>向 {{ group.action.append?.path || '目标文件' }} 写入失败,内容已截获供后续修复。</span>
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="iconStyleSafe('x')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span
|
||||
>向
|
||||
{{ group.action.append?.path || '目标文件' }}
|
||||
写入失败,内容已截获供后续修复。</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<div class="append-meta" v-if="group.action.append">
|
||||
<span v-if="group.action.append.lines !== null && group.action.append.lines !== undefined">
|
||||
<span
|
||||
v-if="
|
||||
group.action.append.lines !== null &&
|
||||
group.action.append.lines !== undefined
|
||||
"
|
||||
>
|
||||
· 行数 {{ group.action.append.lines }}
|
||||
</span>
|
||||
<span v-if="group.action.append.bytes !== null && group.action.append.bytes !== undefined">
|
||||
<span
|
||||
v-if="
|
||||
group.action.append.bytes !== null &&
|
||||
group.action.append.bytes !== undefined
|
||||
"
|
||||
>
|
||||
· 字节 {{ group.action.append.bytes }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="append-warning icon-label" v-if="group.action.append?.forced">
|
||||
<span class="icon icon-sm" :style="iconStyleSafe('triangleAlert')" aria-hidden="true"></span>
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="iconStyleSafe('triangleAlert')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span>未检测到结束标记,请根据提示继续补充。</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -195,16 +279,28 @@
|
||||
>
|
||||
<div class="append-placeholder-content">
|
||||
<div class="icon-label append-status">
|
||||
<span class="icon icon-sm" :style="iconStyleSafe('pencil')" aria-hidden="true"></span>
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="iconStyleSafe('pencil')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span>{{ group.action.append?.summary || '文件追加完成' }}</span>
|
||||
</div>
|
||||
<div class="append-meta" v-if="group.action.append">
|
||||
<span>{{ group.action.append.path || '目标文件' }}</span>
|
||||
<span v-if="group.action.append.lines">· 行数 {{ group.action.append.lines }}</span>
|
||||
<span v-if="group.action.append.bytes">· 字节 {{ group.action.append.bytes }}</span>
|
||||
<span v-if="group.action.append.lines"
|
||||
>· 行数 {{ group.action.append.lines }}</span
|
||||
>
|
||||
<span v-if="group.action.append.bytes"
|
||||
>· 字节 {{ group.action.append.bytes }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="append-warning icon-label" v-if="group.action.append?.forced">
|
||||
<span class="icon icon-sm" :style="iconStyleSafe('triangleAlert')" aria-hidden="true"></span>
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="iconStyleSafe('triangleAlert')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span>未检测到结束标记,请按提示继续补充。</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -213,172 +309,263 @@
|
||||
<ToolAction
|
||||
v-else-if="group.action?.type === 'tool'"
|
||||
:action="group.action"
|
||||
:expanded="expandedBlocks?.has(group.action.blockId || `${index}-tool-${group.actionIndex}`)"
|
||||
:expanded="
|
||||
expandedBlocks?.has(
|
||||
group.action.blockId || `${index}-tool-${group.actionIndex}`
|
||||
)
|
||||
"
|
||||
:icon-style="iconStyleSafe"
|
||||
:get-tool-animation-class="getToolAnimationClass"
|
||||
:get-tool-icon="getToolIcon"
|
||||
:get-tool-status-text="getToolStatusText"
|
||||
:get-tool-description="getToolDescription"
|
||||
:format-search-topic="formatSearchTopic"
|
||||
:format-search-time="formatSearchTime"
|
||||
:format-search-domains="formatSearchDomains"
|
||||
:streaming-message="streamingMessage"
|
||||
:register-collapse-content="registerCollapseContent"
|
||||
:collapse-key="group.action.blockId || `${index}-tool-${group.actionIndex}`"
|
||||
@toggle="toggleBlock(group.action.blockId || `${index}-tool-${group.actionIndex}`)"
|
||||
/>
|
||||
:get-tool-description="getToolDescription"
|
||||
:format-search-topic="formatSearchTopic"
|
||||
:format-search-time="formatSearchTime"
|
||||
:format-search-domains="formatSearchDomains"
|
||||
:streaming-message="streamingMessage"
|
||||
:register-collapse-content="registerCollapseContent"
|
||||
:collapse-key="group.action.blockId || `${index}-tool-${group.actionIndex}`"
|
||||
@toggle="
|
||||
toggleBlock(group.action.blockId || `${index}-tool-${group.actionIndex}`)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
<template
|
||||
v-for="(action, actionIndex) in msg.actions || []"
|
||||
v-if="isActionVisible(action)"
|
||||
:key="action.id || `${index}-${actionIndex}`"
|
||||
class="action-item"
|
||||
:class="{
|
||||
'streaming-content': action.streaming,
|
||||
'completed-tool': action.type === 'tool' && !action.streaming,
|
||||
'immediate-show': action.streaming || action.type === 'text' || action.type === 'thinking',
|
||||
'thinking-finished': action.type === 'thinking' && !action.streaming
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="action.type === 'thinking'"
|
||||
class="collapsible-block thinking-block"
|
||||
:class="{ expanded: expandedBlocks?.has(action.blockId || `${index}-thinking-${actionIndex}`) }"
|
||||
v-if="isActionVisible(action)"
|
||||
class="action-item"
|
||||
:class="{
|
||||
'streaming-content': action.streaming,
|
||||
'completed-tool': action.type === 'tool' && !action.streaming,
|
||||
'immediate-show':
|
||||
action.streaming || action.type === 'text' || action.type === 'thinking',
|
||||
'thinking-finished': action.type === 'thinking' && !action.streaming
|
||||
}"
|
||||
>
|
||||
<div class="collapsible-header" @click="toggleBlock(action.blockId || `${index}-thinking-${actionIndex}`)">
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<span class="thinking-icon" :class="{ 'thinking-animation': action.streaming }">
|
||||
<span class="icon icon-sm" :style="iconStyleSafe('brain')" aria-hidden="true"></span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="status-text">{{ action.streaming ? '正在思考...' : '思考过程' }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="collapsible-content"
|
||||
:ref="el => registerCollapseContent(action.blockId || `${index}-thinking-${actionIndex}`, el)"
|
||||
v-if="action.type === 'thinking'"
|
||||
class="collapsible-block thinking-block"
|
||||
:class="{
|
||||
expanded: expandedBlocks?.has(
|
||||
action.blockId || `${index}-thinking-${actionIndex}`
|
||||
)
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="content-inner thinking-content"
|
||||
:ref="el => registerThinkingRef(action.blockId || `${index}-thinking-${actionIndex}`, el)"
|
||||
@scroll="
|
||||
handleThinkingScroll(
|
||||
action.blockId || `${index}-thinking-${actionIndex}`,
|
||||
$event
|
||||
)
|
||||
"
|
||||
style="max-height: 240px; overflow-y: auto;"
|
||||
class="collapsible-header"
|
||||
@click="toggleBlock(action.blockId || `${index}-thinking-${actionIndex}`)"
|
||||
>
|
||||
{{ action.content }}
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<span
|
||||
class="thinking-icon"
|
||||
:class="{ 'thinking-animation': action.streaming }"
|
||||
>
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="iconStyleSafe('brain')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="status-text">{{
|
||||
action.streaming ? '正在思考...' : '思考过程'
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
class="collapsible-content"
|
||||
:ref="
|
||||
(el) =>
|
||||
registerCollapseContent(
|
||||
action.blockId || `${index}-thinking-${actionIndex}`,
|
||||
el
|
||||
)
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="content-inner thinking-content"
|
||||
:ref="
|
||||
(el) =>
|
||||
registerThinkingRef(
|
||||
action.blockId || `${index}-thinking-${actionIndex}`,
|
||||
el
|
||||
)
|
||||
"
|
||||
@scroll="
|
||||
handleThinkingScroll(
|
||||
action.blockId || `${index}-thinking-${actionIndex}`,
|
||||
$event
|
||||
)
|
||||
"
|
||||
style="max-height: 240px; overflow-y: auto"
|
||||
>
|
||||
{{ action.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="action.type === 'text'" class="text-output">
|
||||
<div class="text-content" :class="{ 'streaming-text': action.streaming }">
|
||||
<div v-if="action.streaming" v-html="renderMarkdown(action.content, true)"></div>
|
||||
<div v-else v-html="renderMarkdown(action.content, false)"></div>
|
||||
<div v-else-if="action.type === 'text'" class="text-output">
|
||||
<div class="text-content" :class="{ 'streaming-text': action.streaming }">
|
||||
<div
|
||||
v-if="action.streaming"
|
||||
v-html="renderMarkdown(action.content, true)"
|
||||
></div>
|
||||
<div v-else v-html="renderMarkdown(action.content, false)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="action.type === 'system' && getSubAgentSystemNoticeLabel(action)"
|
||||
class="sub-agent-system-summary-line"
|
||||
>
|
||||
{{ getSubAgentSystemNoticeLabel(action) }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="action.type === 'system' && getSubAgentSystemNoticeLabel(action)"
|
||||
class="sub-agent-system-summary-line"
|
||||
>
|
||||
{{ getSubAgentSystemNoticeLabel(action) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="action.type === 'append_payload'"
|
||||
class="append-placeholder"
|
||||
:class="{ 'append-error': action.append?.success === false }"
|
||||
>
|
||||
<div class="append-placeholder-content">
|
||||
<template v-if="action.append?.success !== false">
|
||||
<div
|
||||
v-else-if="action.type === 'append_payload'"
|
||||
class="append-placeholder"
|
||||
:class="{ 'append-error': action.append?.success === false }"
|
||||
>
|
||||
<div class="append-placeholder-content">
|
||||
<template v-if="action.append?.success !== false">
|
||||
<div class="icon-label append-status">
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="iconStyleSafe('pencil')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span
|
||||
>已写入
|
||||
{{ action.append?.path || '目标文件' }}
|
||||
的追加内容(内容已保存至文件)</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="icon-label append-status append-error-text">
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="iconStyleSafe('x')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span
|
||||
>向
|
||||
{{ action.append?.path || '目标文件' }}
|
||||
写入失败,内容已截获供后续修复。</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<div class="append-meta" v-if="action.append">
|
||||
<span
|
||||
v-if="action.append.lines !== null && action.append.lines !== undefined"
|
||||
>
|
||||
· 行数 {{ action.append.lines }}
|
||||
</span>
|
||||
<span
|
||||
v-if="action.append.bytes !== null && action.append.bytes !== undefined"
|
||||
>
|
||||
· 字节 {{ action.append.bytes }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="append-warning icon-label" v-if="action.append?.forced">
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="iconStyleSafe('triangleAlert')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span>未检测到结束标记,请根据提示继续补充。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="action.type === 'append'"
|
||||
class="append-placeholder"
|
||||
:class="{ 'append-error': action.append?.success === false }"
|
||||
>
|
||||
<div class="append-placeholder-content">
|
||||
<div class="icon-label append-status">
|
||||
<span class="icon icon-sm" :style="iconStyleSafe('pencil')" aria-hidden="true"></span>
|
||||
<span>已写入 {{ action.append?.path || '目标文件' }} 的追加内容(内容已保存至文件)</span>
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="iconStyleSafe('pencil')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span>{{ action.append?.summary || '文件追加完成' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="icon-label append-status append-error-text">
|
||||
<span class="icon icon-sm" :style="iconStyleSafe('x')" aria-hidden="true"></span>
|
||||
<span>向 {{ action.append?.path || '目标文件' }} 写入失败,内容已截获供后续修复。</span>
|
||||
<div class="append-meta" v-if="action.append">
|
||||
<span>{{ action.append.path || '目标文件' }}</span>
|
||||
<span v-if="action.append.lines">· 行数 {{ action.append.lines }}</span>
|
||||
<span v-if="action.append.bytes">· 字节 {{ action.append.bytes }}</span>
|
||||
</div>
|
||||
<div class="append-warning icon-label" v-if="action.append?.forced">
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="iconStyleSafe('triangleAlert')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span>未检测到结束标记,请按提示继续补充。</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="append-meta" v-if="action.append">
|
||||
<span v-if="action.append.lines !== null && action.append.lines !== undefined">
|
||||
· 行数 {{ action.append.lines }}
|
||||
</span>
|
||||
<span v-if="action.append.bytes !== null && action.append.bytes !== undefined">
|
||||
· 字节 {{ action.append.bytes }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="append-warning icon-label" v-if="action.append?.forced">
|
||||
<span class="icon icon-sm" :style="iconStyleSafe('triangleAlert')" aria-hidden="true"></span>
|
||||
<span>未检测到结束标记,请根据提示继续补充。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="action.type === 'append'"
|
||||
class="append-placeholder"
|
||||
:class="{ 'append-error': action.append?.success === false }"
|
||||
>
|
||||
<div class="append-placeholder-content">
|
||||
<div class="icon-label append-status">
|
||||
<span class="icon icon-sm" :style="iconStyleSafe('pencil')" aria-hidden="true"></span>
|
||||
<span>{{ action.append?.summary || '文件追加完成' }}</span>
|
||||
</div>
|
||||
<div class="append-meta" v-if="action.append">
|
||||
<span>{{ action.append.path || '目标文件' }}</span>
|
||||
<span v-if="action.append.lines">· 行数 {{ action.append.lines }}</span>
|
||||
<span v-if="action.append.bytes">· 字节 {{ action.append.bytes }}</span>
|
||||
</div>
|
||||
<div class="append-warning icon-label" v-if="action.append?.forced">
|
||||
<span class="icon icon-sm" :style="iconStyleSafe('triangleAlert')" aria-hidden="true"></span>
|
||||
<span>未检测到结束标记,请按提示继续补充。</span>
|
||||
</div>
|
||||
</div>
|
||||
<ToolAction
|
||||
v-else-if="action.type === 'tool'"
|
||||
:action="action"
|
||||
:expanded="expandedBlocks?.has(action.blockId || `${index}-tool-${actionIndex}`)"
|
||||
:icon-style="iconStyleSafe"
|
||||
:get-tool-animation-class="getToolAnimationClass"
|
||||
:get-tool-icon="getToolIcon"
|
||||
:get-tool-status-text="getToolStatusText"
|
||||
:get-tool-description="getToolDescription"
|
||||
:format-search-topic="formatSearchTopic"
|
||||
:format-search-time="formatSearchTime"
|
||||
:format-search-domains="formatSearchDomains"
|
||||
:streaming-message="streamingMessage"
|
||||
:register-collapse-content="registerCollapseContent"
|
||||
:collapse-key="action.blockId || `${index}-tool-${actionIndex}`"
|
||||
@toggle="toggleBlock(action.blockId || `${index}-tool-${actionIndex}`)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ToolAction
|
||||
v-else-if="action.type === 'tool'"
|
||||
:action="action"
|
||||
:expanded="expandedBlocks?.has(action.blockId || `${index}-tool-${actionIndex}`)"
|
||||
:icon-style="iconStyleSafe"
|
||||
:get-tool-animation-class="getToolAnimationClass"
|
||||
:get-tool-icon="getToolIcon"
|
||||
:get-tool-status-text="getToolStatusText"
|
||||
:get-tool-description="getToolDescription"
|
||||
:format-search-topic="formatSearchTopic"
|
||||
:format-search-time="formatSearchTime"
|
||||
:format-search-domains="formatSearchDomains"
|
||||
:streaming-message="streamingMessage"
|
||||
:register-collapse-content="registerCollapseContent"
|
||||
:collapse-key="action.blockId || `${index}-tool-${actionIndex}`"
|
||||
@toggle="toggleBlock(action.blockId || `${index}-tool-${actionIndex}`)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="system-message" @vue:mounted="() => console.log('[ChatArea] 渲染系统消息:', { index, role: msg.role, content: msg.content })">
|
||||
<div class="collapsible-block system-block" :class="{ expanded: expandedBlocks?.has(`system-${index}`) }">
|
||||
<div
|
||||
v-else
|
||||
class="system-message"
|
||||
@vue:mounted="
|
||||
() =>
|
||||
console.log('[ChatArea] 渲染系统消息:', {
|
||||
index,
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
})
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="collapsible-block system-block"
|
||||
:class="{ expanded: expandedBlocks?.has(`system-${index}`) }"
|
||||
>
|
||||
<div class="collapsible-header" @click="toggleBlock(`system-${index}`)">
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<span class="tool-icon icon icon-md" :style="iconStyleSafe('info')" aria-hidden="true"></span>
|
||||
<span
|
||||
class="tool-icon icon icon-md"
|
||||
:style="iconStyleSafe('info')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</div>
|
||||
<span class="status-text">系统消息 (role: {{ msg.role }})</span>
|
||||
</div>
|
||||
<div
|
||||
class="collapsible-content"
|
||||
:ref="el => registerCollapseContent(`system-${index}`, el)"
|
||||
:ref="(el) => registerCollapseContent(`system-${index}`, el)"
|
||||
>
|
||||
<div class="content-inner">
|
||||
{{ msg.content }}
|
||||
@ -437,20 +624,29 @@ const userName = computed(() => {
|
||||
});
|
||||
const filteredMessages = computed(() => {
|
||||
const source = props.messages || [];
|
||||
const droppedRoleSystem = source.filter(m => m && m.role === 'system');
|
||||
const droppedRoleSystem = source.filter((m) => m && m.role === 'system');
|
||||
if (droppedRoleSystem.length > 0) {
|
||||
console.log('[DEBUG_SYSTEM][render] filteredMessages 过滤了 role=system 消息', {
|
||||
count: droppedRoleSystem.length,
|
||||
samples: droppedRoleSystem.slice(0, 3)
|
||||
});
|
||||
}
|
||||
return source.filter(m => !(m && m.metadata && m.metadata.system_injected_image) && m.role !== 'system');
|
||||
return source.filter(
|
||||
(m) => !(m && m.metadata && m.metadata.system_injected_image) && m.role !== 'system'
|
||||
);
|
||||
});
|
||||
const latestMessageIndex = computed(() => filteredMessages.value.length - 1);
|
||||
const SUB_AGENT_DONE_LABEL_RE = /^子智能体\d+\s*任务完成$/;
|
||||
const SUB_AGENT_DONE_PREFIX_RE = /^(?:✅\s*)?子智能体\s*#?\s*(\d+)\s*任务摘要[::]/;
|
||||
const BG_RUN_COMMAND_DONE_LABEL_RE = /^(?:\[)?后台\s*run_command\s*完成(?:\])?$/;
|
||||
const RENDERABLE_ACTION_TYPES = new Set(['thinking', 'text', 'tool', 'append', 'append_payload', 'system']);
|
||||
const RENDERABLE_ACTION_TYPES = new Set([
|
||||
'thinking',
|
||||
'text',
|
||||
'tool',
|
||||
'append',
|
||||
'append_payload',
|
||||
'system'
|
||||
]);
|
||||
const nowMs = ref(Date.now());
|
||||
let timerHandle: number | null = null;
|
||||
const debugLoggedSystemActionKeys = new Set<string>();
|
||||
@ -525,7 +721,10 @@ function getSubAgentSystemNoticeLabel(action: any): string | null {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (action.variant === 'sub_agent_done' && (SUB_AGENT_DONE_LABEL_RE.test(content) || BG_RUN_COMMAND_DONE_LABEL_RE.test(content))) {
|
||||
if (
|
||||
action.variant === 'sub_agent_done' &&
|
||||
(SUB_AGENT_DONE_LABEL_RE.test(content) || BG_RUN_COMMAND_DONE_LABEL_RE.test(content))
|
||||
) {
|
||||
if (!debugLoggedSystemActionKeys.has(`${key}-variant-pass`)) {
|
||||
console.log('[DEBUG_SYSTEM][render] 命中 variant 渲染', { action, label: content });
|
||||
debugLoggedSystemActionKeys.add(`${key}-variant-pass`);
|
||||
@ -535,7 +734,10 @@ function getSubAgentSystemNoticeLabel(action: any): string | null {
|
||||
const m = content.match(SUB_AGENT_DONE_PREFIX_RE);
|
||||
if (m && /已完成/.test(content)) {
|
||||
if (!debugLoggedSystemActionKeys.has(`${key}-regex-pass`)) {
|
||||
console.log('[DEBUG_SYSTEM][render] 命中 regex 渲染', { action, normalized: `子智能体${m[1]} 任务完成` });
|
||||
console.log('[DEBUG_SYSTEM][render] 命中 regex 渲染', {
|
||||
action,
|
||||
normalized: `子智能体${m[1]} 任务完成`
|
||||
});
|
||||
debugLoggedSystemActionKeys.add(`${key}-regex-pass`);
|
||||
}
|
||||
return `子智能体${m[1]} 任务完成`;
|
||||
@ -614,9 +816,15 @@ function assistantWorkLabel(index: number): string {
|
||||
if (status === 'working') {
|
||||
return `工作中 ${formatDurationMs(nowMs.value - startMs)}`;
|
||||
}
|
||||
const durationMs = typeof timer.duration_ms === 'number'
|
||||
? timer.duration_ms
|
||||
: Math.max(0, (Number.isFinite(Date.parse(timer.finished_at || '')) ? Date.parse(timer.finished_at) : nowMs.value) - startMs);
|
||||
const durationMs =
|
||||
typeof timer.duration_ms === 'number'
|
||||
? timer.duration_ms
|
||||
: Math.max(
|
||||
0,
|
||||
(Number.isFinite(Date.parse(timer.finished_at || ''))
|
||||
? Date.parse(timer.finished_at)
|
||||
: nowMs.value) - startMs
|
||||
);
|
||||
return `工作完成 ${formatDurationMs(durationMs)}`;
|
||||
}
|
||||
|
||||
@ -633,7 +841,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
});
|
||||
|
||||
const isStackable = (action: any) => action && (action.type === 'thinking' || action.type === 'tool');
|
||||
const isStackable = (action: any) =>
|
||||
action && (action.type === 'thinking' || action.type === 'tool');
|
||||
const isEmptyTextAction = (action: any) => {
|
||||
if (!action || action.type !== 'text') {
|
||||
return false;
|
||||
@ -652,7 +861,7 @@ const splitActionGroups = (actions: any[] = [], messageIndex = 0) => {
|
||||
if (actions.length > 0) {
|
||||
console.log(`[splitActionGroups] 消息 ${messageIndex}:`, {
|
||||
totalActions: actions.length,
|
||||
actionTypes: actions.map(a => a.type),
|
||||
actionTypes: actions.map((a) => a.type),
|
||||
stackedBlocksEnabled: props.stackedBlocksEnabled
|
||||
});
|
||||
}
|
||||
@ -698,7 +907,7 @@ const splitActionGroups = (actions: any[] = [], messageIndex = 0) => {
|
||||
if (actions.length > 0) {
|
||||
console.log(`[splitActionGroups] 消息 ${messageIndex} 结果:`, {
|
||||
totalGroups: result.length,
|
||||
groups: result.map(g => ({
|
||||
groups: result.map((g) => ({
|
||||
kind: g.kind,
|
||||
actionsCount: g.kind === 'stack' ? g.actions.length : 1,
|
||||
actionType: g.kind === 'stack' ? g.actions[0]?.type : g.action?.type
|
||||
|
||||
@ -29,8 +29,20 @@
|
||||
</div>
|
||||
<!-- 加载动画或完成图标 -->
|
||||
<div class="summary-status-icon">
|
||||
<component v-if="isSummaryRunning(group.actions, group.id)" :is="getSummaryLoader(group.id)" />
|
||||
<svg v-else class="check-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<component
|
||||
v-if="isSummaryRunning(group.actions, group.id)"
|
||||
:is="getSummaryLoader(group.id)"
|
||||
/>
|
||||
<svg
|
||||
v-else
|
||||
class="check-icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="7.5 12 10.5 15 16.5 9" />
|
||||
</svg>
|
||||
@ -40,11 +52,7 @@
|
||||
<!-- 步骤容器(展开时显示所有步骤) -->
|
||||
<div class="steps-container" :class="{ show: expandedGroups.has(group.id) }">
|
||||
<div class="steps-wrapper">
|
||||
<div
|
||||
v-for="(step, idx) in getSummarySteps(group.actions)"
|
||||
:key="step.id"
|
||||
class="step-item"
|
||||
>
|
||||
<div v-for="step in getSummarySteps(group.actions)" :key="step.id" class="step-item">
|
||||
<div class="step-timeline">
|
||||
<span
|
||||
class="step-icon icon icon-sm"
|
||||
@ -57,14 +65,16 @@
|
||||
<div v-if="step.type === 'thinking'" class="step-body">
|
||||
<div
|
||||
class="thinking-content"
|
||||
:ref="el => registerThinking(step.id, el)"
|
||||
:ref="(el) => registerThinking(step.id, el)"
|
||||
@scroll="handleScroll(step.id, $event)"
|
||||
v-html="renderContent(step.content)"
|
||||
></div>
|
||||
</div>
|
||||
<div v-else-if="step.type === 'tool'" class="step-body">
|
||||
<div class="step-header">{{ getToolName(step.action) }}</div>
|
||||
<div class="tool-intent" v-if="getToolIntent(step.action)">{{ getToolIntent(step.action) }}</div>
|
||||
<div class="tool-intent" v-if="getToolIntent(step.action)">
|
||||
{{ getToolIntent(step.action) }}
|
||||
</div>
|
||||
<div class="tool-result" v-if="step.action.tool?.result">
|
||||
<div v-html="renderToolResult(step.action)"></div>
|
||||
</div>
|
||||
@ -79,7 +89,11 @@
|
||||
</div>
|
||||
|
||||
<!-- 文本组 -->
|
||||
<div v-else-if="group.type === 'text'" class="text-output" :class="{ 'streaming-text': group.streaming }">
|
||||
<div
|
||||
v-else-if="group.type === 'text'"
|
||||
class="text-output"
|
||||
:class="{ 'streaming-text': group.streaming }"
|
||||
>
|
||||
<div class="text-content" :class="{ 'streaming-text': group.streaming }">
|
||||
<div v-if="group.streaming" v-html="renderMarkdown(group.content || '', true)"></div>
|
||||
<div v-else v-html="renderMarkdown(group.content || '', false)"></div>
|
||||
@ -91,7 +105,15 @@
|
||||
<span class="summary-preview">{{ group.content || '' }}</span>
|
||||
</div>
|
||||
<div class="summary-status-icon">
|
||||
<svg class="check-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
class="check-icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="7.5 12 10.5 15 16.5 9" />
|
||||
</svg>
|
||||
@ -172,11 +194,11 @@ const getAnimatedSummaryChars = (text: string) => Array.from(text || '');
|
||||
|
||||
// 获取摘要组的加载动画组件(每次action类型切换时随机一次)
|
||||
const getSummaryLoader = (groupId: string) => {
|
||||
const group = blockGroups.value.find(g => g.id === groupId);
|
||||
const group = blockGroups.value.find((g) => g.id === groupId);
|
||||
if (!group || !group.actions) return getRandomLoader();
|
||||
|
||||
// 找到当前正在streaming的action,或最后一个action
|
||||
const streamingAction = group.actions.find(a => a.streaming);
|
||||
const streamingAction = group.actions.find((a) => a.streaming);
|
||||
const currentAction = streamingAction || group.actions[group.actions.length - 1];
|
||||
|
||||
if (!currentAction) return getRandomLoader();
|
||||
@ -196,9 +218,10 @@ const getSummaryLoader = (groupId: string) => {
|
||||
// 使用"类型切换点"作为key
|
||||
// 如果是第一个action,或者类型发生了切换,就会生成新的key
|
||||
// 如果是相同类型的连续action(如多个工具并行),使用相同的key
|
||||
const typeSegmentKey = lastDifferentTypeIndex >= 0
|
||||
? `${group.actions[lastDifferentTypeIndex].id}-${currentAction.type}`
|
||||
: `start-${currentAction.type}`;
|
||||
const typeSegmentKey =
|
||||
lastDifferentTypeIndex >= 0
|
||||
? `${group.actions[lastDifferentTypeIndex].id}-${currentAction.type}`
|
||||
: `start-${currentAction.type}`;
|
||||
|
||||
const cacheKey = `${groupId}-${typeSegmentKey}`;
|
||||
|
||||
@ -221,7 +244,8 @@ const blockGroups = computed(() => {
|
||||
// 先保存当前的 thinking/tool 组
|
||||
if (currentGroup.length > 0) {
|
||||
// 使用第一个 action 的 id 作为 group id,保证稳定性
|
||||
const firstActionId = currentGroup[0].id || currentGroup[0].blockId || `summary-${groupIndex}`;
|
||||
const firstActionId =
|
||||
currentGroup[0].id || currentGroup[0].blockId || `summary-${groupIndex}`;
|
||||
groups.push({
|
||||
type: 'summary',
|
||||
id: `summary-${firstActionId}`,
|
||||
@ -270,7 +294,7 @@ const blockGroups = computed(() => {
|
||||
// 获取摘要组的预览文本(始终显示最新的思考/工具)
|
||||
const getSummaryPreview = (actions: Action[]) => {
|
||||
// 优先返回正在 streaming 的最后一个
|
||||
const streamingActions = actions.filter(a => a.streaming);
|
||||
const streamingActions = actions.filter((a) => a.streaming);
|
||||
|
||||
let currentStep;
|
||||
if (streamingActions.length > 0) {
|
||||
@ -278,7 +302,7 @@ const getSummaryPreview = (actions: Action[]) => {
|
||||
currentStep = streamingActions[streamingActions.length - 1];
|
||||
} else {
|
||||
// 如果没有 streaming 的(全部完成了),优先返回最后一个工具
|
||||
const toolActions = actions.filter(a => a.type === 'tool');
|
||||
const toolActions = actions.filter((a) => a.type === 'tool');
|
||||
if (toolActions.length > 0) {
|
||||
currentStep = toolActions[toolActions.length - 1];
|
||||
} else {
|
||||
@ -332,11 +356,11 @@ const isSummaryRunning = (actions: Action[], groupId: string) => {
|
||||
}
|
||||
|
||||
// 找到当前 group 在 blockGroups 中的索引
|
||||
const currentIndex = blockGroups.value.findIndex(g => g.id === groupId);
|
||||
const currentIndex = blockGroups.value.findIndex((g) => g.id === groupId);
|
||||
if (currentIndex === -1) return false;
|
||||
|
||||
// 检查后面是否有文本输出
|
||||
const hasTextAfter = blockGroups.value.slice(currentIndex + 1).some(g => g.type === 'text');
|
||||
const hasTextAfter = blockGroups.value.slice(currentIndex + 1).some((g) => g.type === 'text');
|
||||
|
||||
// 只有当后面有文本输出时,才显示对勾(说明这个摘要组已经完成并开始输出了)
|
||||
// 否则一直显示加载动画(即使工具执行完了,也要等下一个步骤或文本输出)
|
||||
@ -360,9 +384,9 @@ const toggleExpand = (groupId: string) => {
|
||||
|
||||
// 折叠后,取消注册该组中的思考内容 ref
|
||||
nextTick(() => {
|
||||
const group = blockGroups.value.find(g => g.id === groupId);
|
||||
const group = blockGroups.value.find((g) => g.id === groupId);
|
||||
if (group && group.actions) {
|
||||
group.actions.forEach(action => {
|
||||
group.actions.forEach((action) => {
|
||||
if (action.type === 'thinking') {
|
||||
const blockId = action.id || action.blockId;
|
||||
if (blockId && typeof props.registerThinkingRef === 'function') {
|
||||
@ -377,9 +401,9 @@ const toggleExpand = (groupId: string) => {
|
||||
|
||||
// 展开后,注册该组中的思考内容 ref
|
||||
nextTick(() => {
|
||||
const group = blockGroups.value.find(g => g.id === groupId);
|
||||
const group = blockGroups.value.find((g) => g.id === groupId);
|
||||
if (group && group.actions) {
|
||||
group.actions.forEach(action => {
|
||||
group.actions.forEach((action) => {
|
||||
if (action.type === 'thinking') {
|
||||
const blockId = action.id || action.blockId;
|
||||
if (blockId) {
|
||||
@ -487,8 +511,9 @@ const registerThinking = (key: string, el: Element | null) => {
|
||||
}
|
||||
|
||||
// 检查当前思考内容所在的摘要是否展开
|
||||
const group = blockGroups.value.find(g =>
|
||||
g.type === 'summary' && g.actions && g.actions.some(a => (a.id === key || a.blockId === key))
|
||||
const group = blockGroups.value.find(
|
||||
(g) =>
|
||||
g.type === 'summary' && g.actions && g.actions.some((a) => a.id === key || a.blockId === key)
|
||||
);
|
||||
|
||||
// 只有展开的摘要中的思考内容才注册到父组件
|
||||
@ -505,9 +530,13 @@ const handleScroll = (blockId: string, event: Event) => {
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.actions, () => {
|
||||
// 简化:不需要动态更新高度
|
||||
}, { deep: true });
|
||||
watch(
|
||||
() => props.actions,
|
||||
() => {
|
||||
// 简化:不需要动态更新高度
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -30,94 +30,129 @@
|
||||
:style="{ transform: `translateY(${innerOffset}px)` }"
|
||||
appear
|
||||
>
|
||||
<div v-for="(action, idx) in stackableActions" :key="blockKey(action, idx)" class="stacked-item">
|
||||
<div
|
||||
v-if="action.type === 'thinking'"
|
||||
class="collapsible-block thinking-block stacked-block"
|
||||
:class="{ expanded: isExpanded(action, idx), processing: action.streaming }"
|
||||
v-for="(action, idx) in stackableActions"
|
||||
:key="blockKey(action, idx)"
|
||||
class="stacked-item"
|
||||
>
|
||||
<div class="collapsible-header" @click="toggleBlock(blockKey(action, idx))">
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<span class="thinking-icon" :class="{ 'thinking-animation': action.streaming }">
|
||||
<span class="icon icon-sm" :style="iconStyle('brain')" aria-hidden="true"></span>
|
||||
</span>
|
||||
<div
|
||||
v-if="action.type === 'thinking'"
|
||||
class="collapsible-block thinking-block stacked-block"
|
||||
:class="{ expanded: isExpanded(action, idx), processing: action.streaming }"
|
||||
>
|
||||
<div class="collapsible-header" @click="toggleBlock(blockKey(action, idx))">
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<span class="thinking-icon" :class="{ 'thinking-animation': action.streaming }">
|
||||
<span class="icon icon-sm" :style="iconStyle('brain')" aria-hidden="true"></span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="status-text">{{ action.streaming ? '正在思考...' : '思考过程' }}</span>
|
||||
</div>
|
||||
<span class="status-text">{{ action.streaming ? '正在思考...' : '思考过程' }}</span>
|
||||
</div>
|
||||
<div class="collapsible-content" :style="contentStyle(blockKey(action, idx))">
|
||||
<div
|
||||
class="content-inner thinking-content"
|
||||
:ref="el => registerThinking(blockKey(action, idx), el)"
|
||||
@scroll="handleThinkingScrollInternal(blockKey(action, idx), $event)"
|
||||
style="max-height: 240px; overflow-y: auto;"
|
||||
>
|
||||
{{ action.content }}
|
||||
<div class="collapsible-content" :style="contentStyle(blockKey(action, idx))">
|
||||
<div
|
||||
class="content-inner thinking-content"
|
||||
:ref="(el) => registerThinking(blockKey(action, idx), el)"
|
||||
@scroll="handleThinkingScrollInternal(blockKey(action, idx), $event)"
|
||||
style="max-height: 240px; overflow-y: auto"
|
||||
>
|
||||
{{ action.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="action.type === 'tool'"
|
||||
class="collapsible-block tool-block stacked-block"
|
||||
:class="{
|
||||
expanded: isExpanded(action, idx),
|
||||
processing: isToolProcessing(action),
|
||||
completed: isToolCompleted(action)
|
||||
}"
|
||||
>
|
||||
<div class="collapsible-header" @click="toggleBlock(blockKey(action, idx))">
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<span
|
||||
class="tool-icon icon icon-md"
|
||||
:class="getToolAnimationClass(action.tool)"
|
||||
:style="iconStyle(getToolIcon(action.tool))"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<div
|
||||
v-else-if="action.type === 'tool'"
|
||||
class="collapsible-block tool-block stacked-block"
|
||||
:class="{
|
||||
expanded: isExpanded(action, idx),
|
||||
processing: isToolProcessing(action),
|
||||
completed: isToolCompleted(action)
|
||||
}"
|
||||
>
|
||||
<div class="collapsible-header" @click="toggleBlock(blockKey(action, idx))">
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<span
|
||||
class="tool-icon icon icon-md"
|
||||
:class="getToolAnimationClass(action.tool)"
|
||||
:style="iconStyle(getToolIcon(action.tool))"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</div>
|
||||
<span class="status-text">{{ getToolStatusText(action.tool) }}</span>
|
||||
<span class="tool-desc">{{ getToolDescription(action.tool) }}</span>
|
||||
</div>
|
||||
<span class="status-text">{{ getToolStatusText(action.tool) }}</span>
|
||||
<span class="tool-desc">{{ getToolDescription(action.tool) }}</span>
|
||||
</div>
|
||||
<div class="collapsible-content" :style="contentStyle(blockKey(action, idx))">
|
||||
<div class="content-inner">
|
||||
<div v-if="shouldUseEnhancedDisplay && renderToolResult(action)" v-html="renderToolResult(action)"></div>
|
||||
<div v-else-if="!shouldUseEnhancedDisplay && action.tool?.name === 'web_search' && action.tool?.result">
|
||||
<div class="search-meta">
|
||||
<div><strong>搜索内容:</strong>{{ action.tool.result.query || action.tool.arguments?.query }}</div>
|
||||
<div><strong>主题:</strong>{{ formatSearchTopic(action.tool.result.filters || {}) }}</div>
|
||||
<div><strong>时间范围:</strong>{{ formatSearchTime(action.tool.result.filters || {}) }}</div>
|
||||
<div><strong>限定网站:</strong>{{ formatSearchDomains(action.tool.result.filters || {}) }}</div>
|
||||
<div><strong>结果数量:</strong>{{ action.tool.result.total_results }}</div>
|
||||
</div>
|
||||
<div v-if="action.tool.result.results && action.tool.result.results.length" class="search-result-list">
|
||||
<div v-for="item in action.tool.result.results" :key="item.url || item.index" class="search-result-item">
|
||||
<div class="search-result-title">{{ item.title || '无标题' }}</div>
|
||||
<div class="search-result-url">
|
||||
<a v-if="item.url" :href="item.url" target="_blank">{{ item.url }}</a><span v-else>无可用链接</span>
|
||||
<div class="collapsible-content" :style="contentStyle(blockKey(action, idx))">
|
||||
<div class="content-inner">
|
||||
<div
|
||||
v-if="shouldUseEnhancedDisplay && renderToolResult(action)"
|
||||
v-html="renderToolResult(action)"
|
||||
></div>
|
||||
<div
|
||||
v-else-if="
|
||||
!shouldUseEnhancedDisplay &&
|
||||
action.tool?.name === 'web_search' &&
|
||||
action.tool?.result
|
||||
"
|
||||
>
|
||||
<div class="search-meta">
|
||||
<div>
|
||||
<strong>搜索内容:</strong
|
||||
>{{ action.tool.result.query || action.tool.arguments?.query }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>主题:</strong
|
||||
>{{ formatSearchTopic(action.tool.result.filters || {}) }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>时间范围:</strong
|
||||
>{{ formatSearchTime(action.tool.result.filters || {}) }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>限定网站:</strong
|
||||
>{{ formatSearchDomains(action.tool.result.filters || {}) }}
|
||||
</div>
|
||||
<div><strong>结果数量:</strong>{{ action.tool.result.total_results }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="action.tool.result.results && action.tool.result.results.length"
|
||||
class="search-result-list"
|
||||
>
|
||||
<div
|
||||
v-for="item in action.tool.result.results"
|
||||
:key="item.url || item.index"
|
||||
class="search-result-item"
|
||||
>
|
||||
<div class="search-result-title">{{ item.title || '无标题' }}</div>
|
||||
<div class="search-result-url">
|
||||
<a v-if="item.url" :href="item.url" target="_blank">{{ item.url }}</a
|
||||
><span v-else>无可用链接</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="search-empty">未返回详细的搜索结果。</div>
|
||||
</div>
|
||||
<div v-else class="search-empty">未返回详细的搜索结果。</div>
|
||||
</div>
|
||||
<div v-else-if="action.tool?.name === 'run_python' && action.tool?.result">
|
||||
<div class="code-block">
|
||||
<div class="code-label">代码:</div>
|
||||
<pre><code class="language-python">{{ action.tool.result.code || action.tool.arguments?.code }}</code></pre>
|
||||
<div v-else-if="action.tool?.name === 'run_python' && action.tool?.result">
|
||||
<div class="code-block">
|
||||
<div class="code-label">代码:</div>
|
||||
<pre><code class="language-python">{{ action.tool.result.code || action.tool.arguments?.code }}</code></pre>
|
||||
</div>
|
||||
<div v-if="action.tool.result.output" class="output-block">
|
||||
<div class="output-label">输出:</div>
|
||||
<pre>{{ action.tool.result.output }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="action.tool.result.output" class="output-block">
|
||||
<div class="output-label">输出:</div>
|
||||
<pre>{{ action.tool.result.output }}</pre>
|
||||
<div v-else>
|
||||
<pre>{{
|
||||
JSON.stringify(action.tool?.result || action.tool?.arguments, null, 2)
|
||||
}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<pre>{{ JSON.stringify(action.tool?.result || action.tool?.arguments, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isToolProcessing(action)" class="progress-indicator"></div>
|
||||
</div>
|
||||
<div v-if="isToolProcessing(action)" class="progress-indicator"></div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
@ -154,7 +189,12 @@ const shouldUseEnhancedDisplay = computed(() => {
|
||||
});
|
||||
|
||||
const renderToolResult = (action: any) => {
|
||||
return renderEnhancedToolResult(action, props.formatSearchTopic, props.formatSearchTime, props.formatSearchDomains);
|
||||
return renderEnhancedToolResult(
|
||||
action,
|
||||
props.formatSearchTopic,
|
||||
props.formatSearchTime,
|
||||
props.formatSearchDomains
|
||||
);
|
||||
};
|
||||
|
||||
const VISIBLE_LIMIT = 6;
|
||||
@ -175,7 +215,9 @@ let syncRaf: number | null = null;
|
||||
let syncUntil = 0;
|
||||
const isMoreAnimating = ref(false);
|
||||
|
||||
const stackableActions = computed(() => (props.actions || []).filter((item) => item && (item.type === 'thinking' || item.type === 'tool')));
|
||||
const stackableActions = computed(() =>
|
||||
(props.actions || []).filter((item) => item && (item.type === 'thinking' || item.type === 'tool'))
|
||||
);
|
||||
|
||||
const hiddenCount = computed(() => Math.max(0, stackableActions.value.length - VISIBLE_LIMIT));
|
||||
const moreVisible = computed(() => hiddenCount.value > 0 || showAll.value);
|
||||
@ -268,7 +310,8 @@ const setShellMetrics = () => {
|
||||
|
||||
moreHeight.value = moreVisible.value ? moreBaseHeight() : 0;
|
||||
|
||||
const targetShell = moreHeight.value + (showAll.value || !moreVisible.value ? totalHeight : windowHeight);
|
||||
const targetShell =
|
||||
moreHeight.value + (showAll.value || !moreVisible.value ? totalHeight : windowHeight);
|
||||
const targetOffset = showAll.value || !moreVisible.value ? 0 : -hiddenHeight;
|
||||
|
||||
shellHeight.value = targetShell;
|
||||
|
||||
@ -3,16 +3,16 @@
|
||||
<div class="monitor-stage">
|
||||
<div class="monitor">
|
||||
<div class="monitor-shell">
|
||||
<div class="monitor-top">
|
||||
<div class="monitor-brand">Agent Display Surface</div>
|
||||
<div class="status-pill-group">
|
||||
<div class="status-pill">{{ statusLabel }}</div>
|
||||
<div class="progress-pill" v-if="currentProgressLabel">
|
||||
<span class="pulse-dot"></span>
|
||||
<span class="progress-text">{{ currentProgressLabel }}</span>
|
||||
</div>
|
||||
<div class="monitor-top">
|
||||
<div class="monitor-brand">Agent Display Surface</div>
|
||||
<div class="status-pill-group">
|
||||
<div class="status-pill">{{ statusLabel }}</div>
|
||||
<div class="progress-pill" v-if="currentProgressLabel">
|
||||
<span class="pulse-dot"></span>
|
||||
<span class="progress-text">{{ currentProgressLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="monitor-screen" ref="screenEl">
|
||||
<div class="desktop-layer">
|
||||
<div class="desktop-section apps">
|
||||
@ -167,11 +167,19 @@
|
||||
</div>
|
||||
<div class="wait-body">
|
||||
<div class="flip-clock" ref="waitDisplay">
|
||||
<div class="flip-digit" data-value="0"><span class="top">0</span><span class="bottom">0</span></div>
|
||||
<div class="flip-digit" data-value="0"><span class="top">0</span><span class="bottom">0</span></div>
|
||||
<div class="flip-digit" data-value="0">
|
||||
<span class="top">0</span><span class="bottom">0</span>
|
||||
</div>
|
||||
<div class="flip-digit" data-value="0">
|
||||
<span class="top">0</span><span class="bottom">0</span>
|
||||
</div>
|
||||
<div class="flip-separator">:</div>
|
||||
<div class="flip-digit" data-value="0"><span class="top">0</span><span class="bottom">0</span></div>
|
||||
<div class="flip-digit" data-value="0"><span class="top">0</span><span class="bottom">0</span></div>
|
||||
<div class="flip-digit" data-value="0">
|
||||
<span class="top">0</span><span class="bottom">0</span>
|
||||
</div>
|
||||
<div class="flip-digit" data-value="0">
|
||||
<span class="top">0</span><span class="bottom">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -297,21 +305,21 @@ const bubbleIconSlot = ref<HTMLElement | null>(null);
|
||||
const bubbleTextSlot = ref<HTMLElement | null>(null);
|
||||
const mousePointer = ref<HTMLElement | null>(null);
|
||||
|
||||
const mouseIcon = new URL('../../icons/mouse-pointer-2.svg', import.meta.url).href;
|
||||
const mouseIcon = new URL('../../../icons/mouse-pointer-2.svg', import.meta.url).href;
|
||||
|
||||
const assets = {
|
||||
brainIcon: new URL('../../icons/brain.svg', import.meta.url).href,
|
||||
folderIcon: new URL('../../icons/folder.svg', import.meta.url).href,
|
||||
folderOpenIcon: new URL('../../icons/folder-open.svg', import.meta.url).href,
|
||||
fileIcon: new URL('../../icons/file.svg', import.meta.url).href,
|
||||
brainIcon: new URL('../../../icons/brain.svg', import.meta.url).href,
|
||||
folderIcon: new URL('../../../icons/folder.svg', import.meta.url).href,
|
||||
folderOpenIcon: new URL('../../../icons/folder-open.svg', import.meta.url).href,
|
||||
fileIcon: new URL('../../../icons/file.svg', import.meta.url).href,
|
||||
apps: {
|
||||
browser: new URL('../../icons/globe.svg', import.meta.url).href,
|
||||
terminal: new URL('../../icons/laptop.svg', import.meta.url).href,
|
||||
command: new URL('../../icons/terminal.svg', import.meta.url).href,
|
||||
python: new URL('../../icons/python.svg', import.meta.url).href,
|
||||
memory: new URL('../../icons/sticky-note.svg', import.meta.url).href,
|
||||
todo: new URL('../../icons/clipboard.svg', import.meta.url).href,
|
||||
subagent: new URL('../../icons/bot.svg', import.meta.url).href
|
||||
browser: new URL('../../../icons/globe.svg', import.meta.url).href,
|
||||
terminal: new URL('../../../icons/laptop.svg', import.meta.url).href,
|
||||
command: new URL('../../../icons/terminal.svg', import.meta.url).href,
|
||||
python: new URL('../../../icons/python.svg', import.meta.url).href,
|
||||
memory: new URL('../../../icons/sticky-note.svg', import.meta.url).href,
|
||||
todo: new URL('../../../icons/clipboard.svg', import.meta.url).href,
|
||||
subagent: new URL('../../../icons/bot.svg', import.meta.url).href
|
||||
}
|
||||
};
|
||||
|
||||
@ -398,7 +406,7 @@ onMounted(() => {
|
||||
// 监听桌面根目录变化,确保文件/文件夹实时渲染(包括刷新后首帧)
|
||||
watch(
|
||||
lastTreeSnapshot,
|
||||
roots => {
|
||||
(roots) => {
|
||||
if (director.value && Array.isArray(roots)) {
|
||||
director.value.setDesktopRoots(roots, { immediate: true });
|
||||
}
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="action-summary">
|
||||
<div class="summary-header icon-label">
|
||||
<span class="icon icon-sm" :style="iconStyle(action.type === 'apply' ? 'pencil' : 'clipboard')" aria-hidden="true"></span>
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="iconStyle(action.type === 'apply' ? 'pencil' : 'clipboard')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span>{{ action.title || (action.type === 'apply' ? '应用修改' : '追加内容') }}</span>
|
||||
</div>
|
||||
<div class="summary-content" v-if="action.content">
|
||||
@ -9,9 +13,30 @@
|
||||
</div>
|
||||
<div class="summary-actions">
|
||||
<button type="button" class="ghost" @click="$emit('copy', action, blockId)">复制</button>
|
||||
<button type="button" class="ghost" v-if="action.type === 'apply'" @click="$emit('apply', action, blockId)">应用</button>
|
||||
<button type="button" class="ghost" v-if="action.type === 'append'" @click="$emit('run', action, blockId)">执行</button>
|
||||
<button v-if="action.path" type="button" class="ghost" @click="$emit('download', action.path)">下载文件</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ghost"
|
||||
v-if="action.type === 'apply'"
|
||||
@click="$emit('apply', action, blockId)"
|
||||
>
|
||||
应用
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ghost"
|
||||
v-if="action.type === 'append'"
|
||||
@click="$emit('run', action, blockId)"
|
||||
>
|
||||
执行
|
||||
</button>
|
||||
<button
|
||||
v-if="action.path"
|
||||
type="button"
|
||||
class="ghost"
|
||||
@click="$emit('download', action.path)"
|
||||
>
|
||||
下载文件
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -15,8 +15,12 @@
|
||||
</template>
|
||||
<div class="append-meta" v-if="action.append">
|
||||
<span v-if="action.append.path">{{ action.append.path }}</span>
|
||||
<span v-if="action.append.lines !== null && action.append.lines !== undefined">· 行数 {{ action.append.lines }}</span>
|
||||
<span v-if="action.append.bytes !== null && action.append.bytes !== undefined">· 字节 {{ action.append.bytes }}</span>
|
||||
<span v-if="action.append.lines !== null && action.append.lines !== undefined"
|
||||
>· 行数 {{ action.append.lines }}</span
|
||||
>
|
||||
<span v-if="action.append.bytes !== null && action.append.bytes !== undefined"
|
||||
>· 字节 {{ action.append.bytes }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="append-warning icon-label" v-if="action.append?.forced">
|
||||
<span class="icon icon-sm" :style="iconStyle('triangleAlert')" aria-hidden="true"></span>
|
||||
|
||||
@ -18,9 +18,15 @@
|
||||
<pre class="entry-content">{{ entry.content }}</pre>
|
||||
</div>
|
||||
<div class="modify-meta" v-if="action.modify">
|
||||
<span v-if="action.modify.total !== null && action.modify.total !== undefined">· 共 {{ action.modify.total }} 处</span>
|
||||
<span v-if="action.modify.completed && action.modify.completed.length">· 已完成 {{ action.modify.completed.length }} 处</span>
|
||||
<span v-if="action.modify.failed && action.modify.failed.length">· 未完成 {{ action.modify.failed.length }} 处</span>
|
||||
<span v-if="action.modify.total !== null && action.modify.total !== undefined"
|
||||
>· 共 {{ action.modify.total }} 处</span
|
||||
>
|
||||
<span v-if="action.modify.completed && action.modify.completed.length"
|
||||
>· 已完成 {{ action.modify.completed.length }} 处</span
|
||||
>
|
||||
<span v-if="action.modify.failed && action.modify.failed.length"
|
||||
>· 未完成 {{ action.modify.failed.length }} 处</span
|
||||
>
|
||||
</div>
|
||||
<div class="modify-warning icon-label" v-if="action.modify?.forced">
|
||||
<span class="icon icon-sm" :style="iconStyle('triangleAlert')" aria-hidden="true"></span>
|
||||
|
||||
@ -7,8 +7,12 @@
|
||||
</div>
|
||||
<div class="modify-meta" v-if="action.modify">
|
||||
<span v-if="action.modify.total">· 共 {{ action.modify.total }} 处</span>
|
||||
<span v-if="action.modify.completed">· 完成 {{ action.modify.completed.length || action.modify.completed }} 处</span>
|
||||
<span v-if="action.modify.failed">· 未完成 {{ action.modify.failed.length || action.modify.failed }} 处</span>
|
||||
<span v-if="action.modify.completed"
|
||||
>· 完成 {{ action.modify.completed.length || action.modify.completed }} 处</span
|
||||
>
|
||||
<span v-if="action.modify.failed"
|
||||
>· 未完成 {{ action.modify.failed.length || action.modify.failed }} 处</span
|
||||
>
|
||||
</div>
|
||||
<div class="modify-warning icon-label" v-if="action.modify?.forced">
|
||||
<span class="icon icon-sm" :style="iconStyle('triangleAlert')" aria-hidden="true"></span>
|
||||
|
||||
@ -12,9 +12,9 @@
|
||||
<div class="collapsible-content">
|
||||
<div
|
||||
class="content-inner thinking-content"
|
||||
:ref="el => setRef(blockId, el)"
|
||||
@scroll="event => handleScroll(blockId, event)"
|
||||
style="max-height: 240px; overflow-y: auto;"
|
||||
:ref="(el) => setRef(blockId, el)"
|
||||
@scroll="(event) => handleScroll(blockId, event)"
|
||||
style="max-height: 240px; overflow-y: auto"
|
||||
>
|
||||
{{ action.content }}
|
||||
</div>
|
||||
|
||||
@ -22,7 +22,11 @@
|
||||
</div>
|
||||
<div
|
||||
class="collapsible-content"
|
||||
:ref="el => registerCollapseContent && registerCollapseContent(collapseKey || action.tool.id || action.id || 'tool', el)"
|
||||
:ref="
|
||||
(el) =>
|
||||
registerCollapseContent &&
|
||||
registerCollapseContent(collapseKey || action.tool.id || action.id || 'tool', el)
|
||||
"
|
||||
>
|
||||
<div class="content-inner">
|
||||
<div v-if="shouldUseEnhancedDisplay" v-html="renderEnhancedToolResult()"></div>
|
||||
@ -80,7 +84,7 @@ function formatBytes(bytes: number): string {
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function renderEnhancedToolResult(): string {
|
||||
@ -255,10 +259,13 @@ function renderWriteFile(result: any, args: any): string {
|
||||
if (isAppend) {
|
||||
html += `<div><strong>模式:</strong>追加</div>`;
|
||||
}
|
||||
if (!result.success && result.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// 显示写入的内容
|
||||
if (content) {
|
||||
if (result.success && content) {
|
||||
html += '<div class="tool-result-diff scrollable">';
|
||||
const lines = content.split('\n');
|
||||
lines.forEach((line: string) => {
|
||||
@ -284,8 +291,15 @@ function renderEditFile(result: any, args: any): string {
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (!result.success && result.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (!result.success) {
|
||||
return html;
|
||||
}
|
||||
|
||||
// 如果有 replacements 数组,使用新格式
|
||||
if (replacements.length > 0) {
|
||||
html += '<div class="tool-result-diff scrollable">';
|
||||
@ -689,7 +703,7 @@ function renderTodoCreate(result: any, args: any): string {
|
||||
html += '<div class="tool-result-content">';
|
||||
html += '<div class="todo-list">';
|
||||
tasks.forEach((task: any) => {
|
||||
const taskText = typeof task === 'string' ? task : (task.text || task.title || '');
|
||||
const taskText = typeof task === 'string' ? task : task.text || task.title || '';
|
||||
html += `<div class="todo-item"><input type="checkbox" disabled /> ${escapeHtml(taskText)}</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
@ -713,7 +727,7 @@ function renderTodoUpdate(result: any, args: any): string {
|
||||
html += '<div class="tool-result-content">';
|
||||
html += '<div class="todo-list">';
|
||||
tasks.forEach((task: any) => {
|
||||
const taskText = typeof task === 'string' ? task : (task.text || task.title || '');
|
||||
const taskText = typeof task === 'string' ? task : task.text || task.title || '';
|
||||
const completed = typeof task === 'object' && (task.completed || task.done || task.checked);
|
||||
html += `<div class="todo-item"><input type="checkbox" ${completed ? 'checked' : ''} disabled /> ${escapeHtml(taskText)}</div>`;
|
||||
});
|
||||
@ -989,7 +1003,7 @@ function renderEasterEgg(result: any, args: any): string {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.todo-item input[type="checkbox"] {
|
||||
.todo-item input[type='checkbox'] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ export function formatBytes(bytes: number): string {
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
export function renderEnhancedToolResult(
|
||||
@ -89,7 +89,7 @@ export function renderEnhancedToolResult(
|
||||
else if (name === 'todo_create') {
|
||||
return renderTodoCreate(result, args);
|
||||
} else if (name === 'todo_update_task') {
|
||||
return renderTodoUpdate(result, args);
|
||||
return renderTodoUpdate(result);
|
||||
}
|
||||
|
||||
// 彩蛋类
|
||||
@ -200,10 +200,13 @@ function renderWriteFile(result: any, args: any): string {
|
||||
if (isAppend) {
|
||||
html += `<div><strong>模式:</strong>追加</div>`;
|
||||
}
|
||||
if (!result.success && result.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// 显示写入的内容
|
||||
if (content) {
|
||||
if (result.success && content) {
|
||||
html += '<div class="tool-result-diff scrollable">';
|
||||
const lines = content.split('\n');
|
||||
lines.forEach((line: string) => {
|
||||
@ -229,8 +232,15 @@ function renderEditFile(result: any, args: any): string {
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (!result.success && result.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (!result.success) {
|
||||
return html;
|
||||
}
|
||||
|
||||
// 如果有 replacements 数组,使用新格式
|
||||
if (replacements.length > 0) {
|
||||
html += '<div class="tool-result-diff scrollable">';
|
||||
@ -618,7 +628,8 @@ function renderTodoCreate(result: any, args: any): string {
|
||||
html += '<div class="tool-result-content">';
|
||||
html += '<div class="todo-list">';
|
||||
tasks.forEach((task: any) => {
|
||||
const taskText = typeof task === 'string' ? task : (task.text || task.title || task.content || '');
|
||||
const taskText =
|
||||
typeof task === 'string' ? task : task.text || task.title || task.content || '';
|
||||
html += `<div class="todo-item"><input type="checkbox" disabled /> ${escapeHtml(taskText)}</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
@ -628,7 +639,7 @@ function renderTodoCreate(result: any, args: any): string {
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderTodoUpdate(result: any, args: any): string {
|
||||
function renderTodoUpdate(result: any): string {
|
||||
const status = result.success ? '✓ 已更新' : '✗ 失败';
|
||||
const todoList = result.todo_list || {};
|
||||
const title = todoList.title || '';
|
||||
@ -643,8 +654,11 @@ function renderTodoUpdate(result: any, args: any): string {
|
||||
html += '<div class="tool-result-content">';
|
||||
html += '<div class="todo-list">';
|
||||
tasks.forEach((task: any) => {
|
||||
const taskText = typeof task === 'string' ? task : (task.text || task.title || task.content || '');
|
||||
const completed = typeof task === 'object' && (task.status === 'done' || task.completed || task.done || task.checked);
|
||||
const taskText =
|
||||
typeof task === 'string' ? task : task.text || task.title || task.content || '';
|
||||
const completed =
|
||||
typeof task === 'object' &&
|
||||
(task.status === 'done' || task.completed || task.done || task.checked);
|
||||
html += `<div class="todo-item"><input type="checkbox" ${completed ? 'checked' : ''} disabled /> ${escapeHtml(taskText)}</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
.bouncing-squares-loader {
|
||||
--loader-scale: 0.45;
|
||||
--square-size: 7.6px; /* 7.6 * 0.45 ≈ 3.42px,略大一点 */
|
||||
--square-gap: 8.9px; /* 固定间距,缩放后约 4px */
|
||||
--square-gap: 8.9px; /* 固定间距,缩放后约 4px */
|
||||
--start-x: 3px;
|
||||
position: relative;
|
||||
width: 40px;
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
|
||||
.chaotic-orbit-loader::before,
|
||||
.chaotic-orbit-loader::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
@ -29,16 +29,42 @@
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.chaotic-orbit-loader::before { animation: orbit var(--speed) linear infinite; }
|
||||
.chaotic-orbit-loader::after { animation: orbit var(--speed) linear calc(var(--speed) / -2) infinite; }
|
||||
.chaotic-orbit-loader::before {
|
||||
animation: orbit var(--speed) linear infinite;
|
||||
}
|
||||
.chaotic-orbit-loader::after {
|
||||
animation: orbit var(--speed) linear calc(var(--speed) / -2) infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes orbit {
|
||||
0% { transform: translate(12px) scale(.74); opacity:.65; }
|
||||
25% { transform: translate(0) scale(.47); opacity:.3; }
|
||||
50% { transform: translate(-12px) scale(.74); opacity:.65; }
|
||||
75% { transform: translate(0) scale(1); opacity:1; }
|
||||
100% { transform: translate(12px) scale(.74); opacity:.65; }
|
||||
0% {
|
||||
transform: translate(12px) scale(0.74);
|
||||
opacity: 0.65;
|
||||
}
|
||||
25% {
|
||||
transform: translate(0) scale(0.47);
|
||||
opacity: 0.3;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-12px) scale(0.74);
|
||||
opacity: 0.65;
|
||||
}
|
||||
75% {
|
||||
transform: translate(0) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate(12px) scale(0.74);
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
<style scoped>
|
||||
.dot-spinner-loader {
|
||||
--size: 22px;
|
||||
--speed: .9s;
|
||||
--speed: 0.9s;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -39,32 +39,67 @@
|
||||
}
|
||||
|
||||
.dot::before {
|
||||
content: "";
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background: var(--claude-text-tertiary);
|
||||
transform: scale(0);
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
animation: pulse calc(var(--speed) * 1.111) ease-in-out infinite;
|
||||
}
|
||||
|
||||
.dot:nth-child(2) { transform: rotate(45deg); }
|
||||
.dot:nth-child(2)::before { animation-delay: calc(var(--speed) * -0.875); }
|
||||
.dot:nth-child(3) { transform: rotate(90deg); }
|
||||
.dot:nth-child(3)::before { animation-delay: calc(var(--speed) * -0.75); }
|
||||
.dot:nth-child(4) { transform: rotate(135deg); }
|
||||
.dot:nth-child(4)::before { animation-delay: calc(var(--speed) * -0.625); }
|
||||
.dot:nth-child(5) { transform: rotate(180deg); }
|
||||
.dot:nth-child(5)::before { animation-delay: calc(var(--speed) * -0.5); }
|
||||
.dot:nth-child(6) { transform: rotate(225deg); }
|
||||
.dot:nth-child(6)::before { animation-delay: calc(var(--speed) * -0.375); }
|
||||
.dot:nth-child(7) { transform: rotate(270deg); }
|
||||
.dot:nth-child(7)::before { animation-delay: calc(var(--speed) * -0.25); }
|
||||
.dot:nth-child(8) { transform: rotate(315deg); }
|
||||
.dot:nth-child(8)::before { animation-delay: calc(var(--speed) * -0.125); }
|
||||
.dot:nth-child(2) {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.dot:nth-child(2)::before {
|
||||
animation-delay: calc(var(--speed) * -0.875);
|
||||
}
|
||||
.dot:nth-child(3) {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.dot:nth-child(3)::before {
|
||||
animation-delay: calc(var(--speed) * -0.75);
|
||||
}
|
||||
.dot:nth-child(4) {
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
.dot:nth-child(4)::before {
|
||||
animation-delay: calc(var(--speed) * -0.625);
|
||||
}
|
||||
.dot:nth-child(5) {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.dot:nth-child(5)::before {
|
||||
animation-delay: calc(var(--speed) * -0.5);
|
||||
}
|
||||
.dot:nth-child(6) {
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
.dot:nth-child(6)::before {
|
||||
animation-delay: calc(var(--speed) * -0.375);
|
||||
}
|
||||
.dot:nth-child(7) {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
.dot:nth-child(7)::before {
|
||||
animation-delay: calc(var(--speed) * -0.25);
|
||||
}
|
||||
.dot:nth-child(8) {
|
||||
transform: rotate(315deg);
|
||||
}
|
||||
.dot:nth-child(8)::before {
|
||||
animation-delay: calc(var(--speed) * -0.125);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(0); opacity: .5; }
|
||||
50% { transform: scale(1); opacity: 1; }
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
.dual-chase-loader::before {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
content: "";
|
||||
content: '';
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
top: 50%;
|
||||
@ -34,7 +34,8 @@
|
||||
}
|
||||
|
||||
@keyframes chase-down {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
@ -52,7 +53,8 @@
|
||||
}
|
||||
|
||||
@keyframes chase-up {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
|
||||
@ -60,48 +60,142 @@
|
||||
}
|
||||
|
||||
@keyframes square1 {
|
||||
0% { left: 0; top: 0; }
|
||||
8.33% { left: 0; top: var(--offset); }
|
||||
100% { left: 0; top: var(--offset); }
|
||||
0% {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
8.33% {
|
||||
left: 0;
|
||||
top: var(--offset);
|
||||
}
|
||||
100% {
|
||||
left: 0;
|
||||
top: var(--offset);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes square2 {
|
||||
0% { left: 0; top: var(--offset); }
|
||||
8.33% { left: 0; top: calc(2 * var(--offset)); }
|
||||
16.67% { left: var(--offset); top: calc(2 * var(--offset)); }
|
||||
25% { left: var(--offset); top: var(--offset); }
|
||||
83.33% { left: var(--offset); top: var(--offset); }
|
||||
91.67% { left: var(--offset); top: 0; }
|
||||
100% { left: 0; top: 0; }
|
||||
0% {
|
||||
left: 0;
|
||||
top: var(--offset);
|
||||
}
|
||||
8.33% {
|
||||
left: 0;
|
||||
top: calc(2 * var(--offset));
|
||||
}
|
||||
16.67% {
|
||||
left: var(--offset);
|
||||
top: calc(2 * var(--offset));
|
||||
}
|
||||
25% {
|
||||
left: var(--offset);
|
||||
top: var(--offset);
|
||||
}
|
||||
83.33% {
|
||||
left: var(--offset);
|
||||
top: var(--offset);
|
||||
}
|
||||
91.67% {
|
||||
left: var(--offset);
|
||||
top: 0;
|
||||
}
|
||||
100% {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes square3 {
|
||||
0%, 100% { left: var(--offset); top: var(--offset); }
|
||||
16.67% { left: var(--offset); top: var(--offset); }
|
||||
25% { left: var(--offset); top: 0; }
|
||||
33.33% { left: calc(2 * var(--offset)); top: 0; }
|
||||
41.67% { left: calc(2 * var(--offset)); top: var(--offset); }
|
||||
66.67% { left: calc(2 * var(--offset)); top: var(--offset); }
|
||||
75% { left: calc(2 * var(--offset)); top: calc(2 * var(--offset)); }
|
||||
83.33% { left: var(--offset); top: calc(2 * var(--offset)); }
|
||||
91.67% { left: var(--offset); top: var(--offset); }
|
||||
0%,
|
||||
100% {
|
||||
left: var(--offset);
|
||||
top: var(--offset);
|
||||
}
|
||||
16.67% {
|
||||
left: var(--offset);
|
||||
top: var(--offset);
|
||||
}
|
||||
25% {
|
||||
left: var(--offset);
|
||||
top: 0;
|
||||
}
|
||||
33.33% {
|
||||
left: calc(2 * var(--offset));
|
||||
top: 0;
|
||||
}
|
||||
41.67% {
|
||||
left: calc(2 * var(--offset));
|
||||
top: var(--offset);
|
||||
}
|
||||
66.67% {
|
||||
left: calc(2 * var(--offset));
|
||||
top: var(--offset);
|
||||
}
|
||||
75% {
|
||||
left: calc(2 * var(--offset));
|
||||
top: calc(2 * var(--offset));
|
||||
}
|
||||
83.33% {
|
||||
left: var(--offset);
|
||||
top: calc(2 * var(--offset));
|
||||
}
|
||||
91.67% {
|
||||
left: var(--offset);
|
||||
top: var(--offset);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes square4 {
|
||||
0% { left: calc(2 * var(--offset)); top: var(--offset); }
|
||||
33.33% { left: calc(2 * var(--offset)); top: var(--offset); }
|
||||
41.67% { left: calc(2 * var(--offset)); top: calc(2 * var(--offset)); }
|
||||
50% { left: calc(3 * var(--offset)); top: calc(2 * var(--offset)); }
|
||||
58.33% { left: calc(3 * var(--offset)); top: var(--offset); }
|
||||
100% { left: calc(3 * var(--offset)); top: var(--offset); }
|
||||
0% {
|
||||
left: calc(2 * var(--offset));
|
||||
top: var(--offset);
|
||||
}
|
||||
33.33% {
|
||||
left: calc(2 * var(--offset));
|
||||
top: var(--offset);
|
||||
}
|
||||
41.67% {
|
||||
left: calc(2 * var(--offset));
|
||||
top: calc(2 * var(--offset));
|
||||
}
|
||||
50% {
|
||||
left: calc(3 * var(--offset));
|
||||
top: calc(2 * var(--offset));
|
||||
}
|
||||
58.33% {
|
||||
left: calc(3 * var(--offset));
|
||||
top: var(--offset);
|
||||
}
|
||||
100% {
|
||||
left: calc(3 * var(--offset));
|
||||
top: var(--offset);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes square5 {
|
||||
0% { left: calc(3 * var(--offset)); top: var(--offset); }
|
||||
50% { left: calc(3 * var(--offset)); top: var(--offset); }
|
||||
58.33% { left: calc(3 * var(--offset)); top: 0; }
|
||||
66.67% { left: calc(2 * var(--offset)); top: 0; }
|
||||
75% { left: calc(2 * var(--offset)); top: var(--offset); }
|
||||
100% { left: calc(2 * var(--offset)); top: var(--offset); }
|
||||
0% {
|
||||
left: calc(3 * var(--offset));
|
||||
top: var(--offset);
|
||||
}
|
||||
50% {
|
||||
left: calc(3 * var(--offset));
|
||||
top: var(--offset);
|
||||
}
|
||||
58.33% {
|
||||
left: calc(3 * var(--offset));
|
||||
top: 0;
|
||||
}
|
||||
66.67% {
|
||||
left: calc(2 * var(--offset));
|
||||
top: 0;
|
||||
}
|
||||
75% {
|
||||
left: calc(2 * var(--offset));
|
||||
top: var(--offset);
|
||||
}
|
||||
100% {
|
||||
left: calc(2 * var(--offset));
|
||||
top: var(--offset);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -29,14 +29,34 @@
|
||||
animation: follow-track 1.25s infinite;
|
||||
}
|
||||
|
||||
.sq:nth-child(1) { top: 0; animation-delay: 0.15s; }
|
||||
.sq:nth-child(2) { top: 5px; animation-delay: 0.3s; }
|
||||
.sq:nth-child(3) { top: 10px; animation-delay: 0.45s; }
|
||||
.sq:nth-child(4) { top: 15px; animation-delay: 0.6s; }
|
||||
.sq:nth-child(5) { top: 20px; animation-delay: 0.75s; }
|
||||
.sq:nth-child(1) {
|
||||
top: 0;
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
.sq:nth-child(2) {
|
||||
top: 5px;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
.sq:nth-child(3) {
|
||||
top: 10px;
|
||||
animation-delay: 0.45s;
|
||||
}
|
||||
.sq:nth-child(4) {
|
||||
top: 15px;
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
.sq:nth-child(5) {
|
||||
top: 20px;
|
||||
animation-delay: 0.75s;
|
||||
}
|
||||
|
||||
@keyframes follow-track {
|
||||
0% { transform: rotate(0deg) translateY(-4px); }
|
||||
60%, 100% { transform: rotate(360deg) translateY(-4px); }
|
||||
0% {
|
||||
transform: rotate(0deg) translateY(-4px);
|
||||
}
|
||||
60%,
|
||||
100% {
|
||||
transform: rotate(360deg) translateY(-4px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -30,15 +30,32 @@
|
||||
animation: corner-cycle 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.sq:nth-child(1) { animation-delay: -0.45s; }
|
||||
.sq:nth-child(2) { animation-delay: -0.9s; }
|
||||
.sq:nth-child(3) { animation-delay: -1.35s; }
|
||||
.sq:nth-child(4) { animation-delay: -1.8s; }
|
||||
.sq:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
.sq:nth-child(2) {
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
.sq:nth-child(3) {
|
||||
animation-delay: -1.35s;
|
||||
}
|
||||
.sq:nth-child(4) {
|
||||
animation-delay: -1.8s;
|
||||
}
|
||||
|
||||
@keyframes corner-cycle {
|
||||
0%, 100% { transform: translate(-6px, -6px); }
|
||||
25% { transform: translate(6px, -6px); }
|
||||
50% { transform: translate(6px, 6px); }
|
||||
75% { transform: translate(-6px, 6px); }
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(-6px, -6px);
|
||||
}
|
||||
25% {
|
||||
transform: translate(6px, -6px);
|
||||
}
|
||||
50% {
|
||||
transform: translate(6px, 6px);
|
||||
}
|
||||
75% {
|
||||
transform: translate(-6px, 6px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -26,13 +26,26 @@
|
||||
animation: jump 2s ease infinite;
|
||||
}
|
||||
|
||||
.sq:nth-child(2n) { animation-delay: 300ms; }
|
||||
.sq:nth-child(3n) { animation-delay: 600ms; }
|
||||
.sq:nth-child(2n) {
|
||||
animation-delay: 300ms;
|
||||
}
|
||||
.sq:nth-child(3n) {
|
||||
animation-delay: 600ms;
|
||||
}
|
||||
|
||||
@keyframes jump {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
25% { transform: translateY(-10px) scale(0.66); }
|
||||
50% { transform: translateY(0); }
|
||||
75% { transform: translateY(3px) scale(0.9); }
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-10px) scale(0.66);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(3px) scale(0.9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
.dot::before {
|
||||
content: "";
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background: var(--claude-text-tertiary);
|
||||
@ -52,10 +52,20 @@
|
||||
}
|
||||
|
||||
@keyframes leap {
|
||||
0% { transform: translateX(0) rotate(0deg); }
|
||||
33.333% { transform: translateX(0) rotate(180deg); }
|
||||
66.666% { transform: translateX(calc(var(--size) * -0.4)) rotate(180deg); }
|
||||
99.999% { transform: translateX(calc(var(--size) * -0.8)) rotate(180deg); }
|
||||
100% { transform: translateX(0) rotate(0deg); }
|
||||
0% {
|
||||
transform: translateX(0) rotate(0deg);
|
||||
}
|
||||
33.333% {
|
||||
transform: translateX(0) rotate(180deg);
|
||||
}
|
||||
66.666% {
|
||||
transform: translateX(calc(var(--size) * -0.4)) rotate(180deg);
|
||||
}
|
||||
99.999% {
|
||||
transform: translateX(calc(var(--size) * -0.8)) rotate(180deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0) rotate(0deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
}
|
||||
|
||||
.dot::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 75%;
|
||||
width: 3px;
|
||||
@ -48,14 +48,32 @@
|
||||
}
|
||||
|
||||
@keyframes swing {
|
||||
0% { transform: rotate(0deg); animation-timing-function: ease-out; }
|
||||
25% { transform: rotate(70deg); animation-timing-function: ease-in; }
|
||||
50% { transform: rotate(0deg); animation-timing-function: linear; }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
25% {
|
||||
transform: rotate(70deg);
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
50% {
|
||||
transform: rotate(0deg);
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes swing2 {
|
||||
0% { transform: rotate(0deg); animation-timing-function: linear; }
|
||||
50% { transform: rotate(0deg); animation-timing-function: ease-out; }
|
||||
75% { transform: rotate(-70deg); animation-timing-function: ease-in; }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
50% {
|
||||
transform: rotate(0deg);
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
75% {
|
||||
transform: rotate(-70deg);
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
.box:before {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
@ -59,139 +59,355 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.box:nth-child(1) { animation: moveBox-1 4s infinite; }
|
||||
.box:nth-child(2) { animation: moveBox-2 4s infinite; }
|
||||
.box:nth-child(3) { animation: moveBox-3 4s infinite; }
|
||||
.box:nth-child(4) { animation: moveBox-4 4s infinite; }
|
||||
.box:nth-child(5) { animation: moveBox-5 4s infinite; }
|
||||
.box:nth-child(6) { animation: moveBox-6 4s infinite; }
|
||||
.box:nth-child(7) { animation: moveBox-7 4s infinite; }
|
||||
.box:nth-child(8) { animation: moveBox-8 4s infinite; }
|
||||
.box:nth-child(9) { animation: moveBox-9 4s infinite; }
|
||||
.box:nth-child(1) {
|
||||
animation: moveBox-1 4s infinite;
|
||||
}
|
||||
.box:nth-child(2) {
|
||||
animation: moveBox-2 4s infinite;
|
||||
}
|
||||
.box:nth-child(3) {
|
||||
animation: moveBox-3 4s infinite;
|
||||
}
|
||||
.box:nth-child(4) {
|
||||
animation: moveBox-4 4s infinite;
|
||||
}
|
||||
.box:nth-child(5) {
|
||||
animation: moveBox-5 4s infinite;
|
||||
}
|
||||
.box:nth-child(6) {
|
||||
animation: moveBox-6 4s infinite;
|
||||
}
|
||||
.box:nth-child(7) {
|
||||
animation: moveBox-7 4s infinite;
|
||||
}
|
||||
.box:nth-child(8) {
|
||||
animation: moveBox-8 4s infinite;
|
||||
}
|
||||
.box:nth-child(9) {
|
||||
animation: moveBox-9 4s infinite;
|
||||
}
|
||||
|
||||
@keyframes moveBox-1 {
|
||||
9% { transform: translate(-7px, 0); }
|
||||
18% { transform: translate(0, 0); }
|
||||
27% { transform: translate(0, 0); }
|
||||
36% { transform: translate(7px, 0); }
|
||||
45% { transform: translate(7px, 7px); }
|
||||
54% { transform: translate(7px, 7px); }
|
||||
63% { transform: translate(7px, 7px); }
|
||||
72% { transform: translate(7px, 0); }
|
||||
81% { transform: translate(0, 0); }
|
||||
90% { transform: translate(-7px, 0); }
|
||||
100% { transform: translate(0, 0); }
|
||||
9% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
18% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
27% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
36% {
|
||||
transform: translate(7px, 0);
|
||||
}
|
||||
45% {
|
||||
transform: translate(7px, 7px);
|
||||
}
|
||||
54% {
|
||||
transform: translate(7px, 7px);
|
||||
}
|
||||
63% {
|
||||
transform: translate(7px, 7px);
|
||||
}
|
||||
72% {
|
||||
transform: translate(7px, 0);
|
||||
}
|
||||
81% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveBox-2 {
|
||||
9% { transform: translate(0, 0); }
|
||||
18% { transform: translate(7px, 0); }
|
||||
27% { transform: translate(0, 0); }
|
||||
36% { transform: translate(7px, 0); }
|
||||
45% { transform: translate(7px, 7px); }
|
||||
54% { transform: translate(7px, 7px); }
|
||||
63% { transform: translate(7px, 7px); }
|
||||
72% { transform: translate(7px, 7px); }
|
||||
81% { transform: translate(0, 7px); }
|
||||
90% { transform: translate(0, 7px); }
|
||||
100% { transform: translate(0, 0); }
|
||||
9% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
18% {
|
||||
transform: translate(7px, 0);
|
||||
}
|
||||
27% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
36% {
|
||||
transform: translate(7px, 0);
|
||||
}
|
||||
45% {
|
||||
transform: translate(7px, 7px);
|
||||
}
|
||||
54% {
|
||||
transform: translate(7px, 7px);
|
||||
}
|
||||
63% {
|
||||
transform: translate(7px, 7px);
|
||||
}
|
||||
72% {
|
||||
transform: translate(7px, 7px);
|
||||
}
|
||||
81% {
|
||||
transform: translate(0, 7px);
|
||||
}
|
||||
90% {
|
||||
transform: translate(0, 7px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveBox-3 {
|
||||
9% { transform: translate(-7px, 0); }
|
||||
18% { transform: translate(-7px, 0); }
|
||||
27% { transform: translate(0, 0); }
|
||||
36% { transform: translate(-7px, 0); }
|
||||
45% { transform: translate(-7px, 0); }
|
||||
54% { transform: translate(-7px, 0); }
|
||||
63% { transform: translate(-7px, 0); }
|
||||
72% { transform: translate(-7px, 0); }
|
||||
81% { transform: translate(-7px, -7px); }
|
||||
90% { transform: translate(0, -7px); }
|
||||
100% { transform: translate(0, 0); }
|
||||
9% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
18% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
27% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
36% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
45% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
54% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
63% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
72% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
81% {
|
||||
transform: translate(-7px, -7px);
|
||||
}
|
||||
90% {
|
||||
transform: translate(0, -7px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveBox-4 {
|
||||
9% { transform: translate(-7px, 0); }
|
||||
18% { transform: translate(-7px, 0); }
|
||||
27% { transform: translate(-7px, -7px); }
|
||||
36% { transform: translate(0, -7px); }
|
||||
45% { transform: translate(0, 0); }
|
||||
54% { transform: translate(0, -7px); }
|
||||
63% { transform: translate(0, -7px); }
|
||||
72% { transform: translate(0, -7px); }
|
||||
81% { transform: translate(-7px, -7px); }
|
||||
90% { transform: translate(-7px, 0); }
|
||||
100% { transform: translate(0, 0); }
|
||||
9% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
18% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
27% {
|
||||
transform: translate(-7px, -7px);
|
||||
}
|
||||
36% {
|
||||
transform: translate(0, -7px);
|
||||
}
|
||||
45% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
54% {
|
||||
transform: translate(0, -7px);
|
||||
}
|
||||
63% {
|
||||
transform: translate(0, -7px);
|
||||
}
|
||||
72% {
|
||||
transform: translate(0, -7px);
|
||||
}
|
||||
81% {
|
||||
transform: translate(-7px, -7px);
|
||||
}
|
||||
90% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveBox-5 {
|
||||
9% { transform: translate(0, 0); }
|
||||
18% { transform: translate(0, 0); }
|
||||
27% { transform: translate(0, 0); }
|
||||
36% { transform: translate(7px, 0); }
|
||||
45% { transform: translate(7px, 0); }
|
||||
54% { transform: translate(7px, 0); }
|
||||
63% { transform: translate(7px, 0); }
|
||||
72% { transform: translate(7px, 0); }
|
||||
81% { transform: translate(7px, -7px); }
|
||||
90% { transform: translate(0, -7px); }
|
||||
100% { transform: translate(0, 0); }
|
||||
9% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
18% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
27% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
36% {
|
||||
transform: translate(7px, 0);
|
||||
}
|
||||
45% {
|
||||
transform: translate(7px, 0);
|
||||
}
|
||||
54% {
|
||||
transform: translate(7px, 0);
|
||||
}
|
||||
63% {
|
||||
transform: translate(7px, 0);
|
||||
}
|
||||
72% {
|
||||
transform: translate(7px, 0);
|
||||
}
|
||||
81% {
|
||||
transform: translate(7px, -7px);
|
||||
}
|
||||
90% {
|
||||
transform: translate(0, -7px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveBox-6 {
|
||||
9% { transform: translate(0, 0); }
|
||||
18% { transform: translate(-7px, 0); }
|
||||
27% { transform: translate(-7px, 0); }
|
||||
36% { transform: translate(0, 0); }
|
||||
45% { transform: translate(0, 0); }
|
||||
54% { transform: translate(0, 0); }
|
||||
63% { transform: translate(0, 0); }
|
||||
72% { transform: translate(0, 7px); }
|
||||
81% { transform: translate(-7px, 7px); }
|
||||
90% { transform: translate(-7px, 0); }
|
||||
100% { transform: translate(0, 0); }
|
||||
9% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
18% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
27% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
36% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
45% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
54% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
63% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
72% {
|
||||
transform: translate(0, 7px);
|
||||
}
|
||||
81% {
|
||||
transform: translate(-7px, 7px);
|
||||
}
|
||||
90% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveBox-7 {
|
||||
9% { transform: translate(7px, 0); }
|
||||
18% { transform: translate(7px, 0); }
|
||||
27% { transform: translate(7px, 0); }
|
||||
36% { transform: translate(0, 0); }
|
||||
45% { transform: translate(0, -7px); }
|
||||
54% { transform: translate(7px, -7px); }
|
||||
63% { transform: translate(0, -7px); }
|
||||
72% { transform: translate(0, -7px); }
|
||||
81% { transform: translate(0, 0); }
|
||||
90% { transform: translate(7px, 0); }
|
||||
100% { transform: translate(0, 0); }
|
||||
9% {
|
||||
transform: translate(7px, 0);
|
||||
}
|
||||
18% {
|
||||
transform: translate(7px, 0);
|
||||
}
|
||||
27% {
|
||||
transform: translate(7px, 0);
|
||||
}
|
||||
36% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
45% {
|
||||
transform: translate(0, -7px);
|
||||
}
|
||||
54% {
|
||||
transform: translate(7px, -7px);
|
||||
}
|
||||
63% {
|
||||
transform: translate(0, -7px);
|
||||
}
|
||||
72% {
|
||||
transform: translate(0, -7px);
|
||||
}
|
||||
81% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate(7px, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveBox-8 {
|
||||
9% { transform: translate(0, 0); }
|
||||
18% { transform: translate(-7px, 0); }
|
||||
27% { transform: translate(-7px, -7px); }
|
||||
36% { transform: translate(0, -7px); }
|
||||
45% { transform: translate(0, -7px); }
|
||||
54% { transform: translate(0, -7px); }
|
||||
63% { transform: translate(0, -7px); }
|
||||
72% { transform: translate(0, -7px); }
|
||||
81% { transform: translate(7px, -7px); }
|
||||
90% { transform: translate(7px, 0); }
|
||||
100% { transform: translate(0, 0); }
|
||||
9% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
18% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
27% {
|
||||
transform: translate(-7px, -7px);
|
||||
}
|
||||
36% {
|
||||
transform: translate(0, -7px);
|
||||
}
|
||||
45% {
|
||||
transform: translate(0, -7px);
|
||||
}
|
||||
54% {
|
||||
transform: translate(0, -7px);
|
||||
}
|
||||
63% {
|
||||
transform: translate(0, -7px);
|
||||
}
|
||||
72% {
|
||||
transform: translate(0, -7px);
|
||||
}
|
||||
81% {
|
||||
transform: translate(7px, -7px);
|
||||
}
|
||||
90% {
|
||||
transform: translate(7px, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveBox-9 {
|
||||
9% { transform: translate(-7px, 0); }
|
||||
18% { transform: translate(-7px, 0); }
|
||||
27% { transform: translate(0, 0); }
|
||||
36% { transform: translate(-7px, 0); }
|
||||
45% { transform: translate(0, 0); }
|
||||
54% { transform: translate(0, 0); }
|
||||
63% { transform: translate(-7px, 0); }
|
||||
72% { transform: translate(-7px, 0); }
|
||||
81% { transform: translate(-14px, 0); }
|
||||
90% { transform: translate(-7px, 0); }
|
||||
100% { transform: translate(0, 0); }
|
||||
9% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
18% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
27% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
36% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
45% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
54% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
63% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
72% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
81% {
|
||||
transform: translate(-14px, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate(-7px, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
.orbe::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@ -43,8 +43,13 @@
|
||||
}
|
||||
|
||||
@keyframes orbit {
|
||||
0% {}
|
||||
80% { transform: rotate(360deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
}
|
||||
80% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -16,23 +16,74 @@
|
||||
|
||||
<style scoped>
|
||||
@keyframes square-animation {
|
||||
0% { left: 0; top: 0; }
|
||||
10.5% { left: 0; top: 0; }
|
||||
12.5% { left: 8px; top: 0; }
|
||||
23% { left: 8px; top: 0; }
|
||||
25% { left: 16px; top: 0; }
|
||||
35.5% { left: 16px; top: 0; }
|
||||
37.5% { left: 16px; top: 8px; }
|
||||
48% { left: 16px; top: 8px; }
|
||||
50% { left: 8px; top: 8px; }
|
||||
60.5% { left: 8px; top: 8px; }
|
||||
62.5% { left: 8px; top: 16px; }
|
||||
73% { left: 8px; top: 16px; }
|
||||
75% { left: 0; top: 16px; }
|
||||
85.5% { left: 0; top: 16px; }
|
||||
87.5% { left: 0; top: 8px; }
|
||||
98% { left: 0; top: 8px; }
|
||||
100% { left: 0; top: 0; }
|
||||
0% {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
10.5% {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
12.5% {
|
||||
left: 8px;
|
||||
top: 0;
|
||||
}
|
||||
23% {
|
||||
left: 8px;
|
||||
top: 0;
|
||||
}
|
||||
25% {
|
||||
left: 16px;
|
||||
top: 0;
|
||||
}
|
||||
35.5% {
|
||||
left: 16px;
|
||||
top: 0;
|
||||
}
|
||||
37.5% {
|
||||
left: 16px;
|
||||
top: 8px;
|
||||
}
|
||||
48% {
|
||||
left: 16px;
|
||||
top: 8px;
|
||||
}
|
||||
50% {
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
}
|
||||
60.5% {
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
}
|
||||
62.5% {
|
||||
left: 8px;
|
||||
top: 16px;
|
||||
}
|
||||
73% {
|
||||
left: 8px;
|
||||
top: 16px;
|
||||
}
|
||||
75% {
|
||||
left: 0;
|
||||
top: 16px;
|
||||
}
|
||||
85.5% {
|
||||
left: 0;
|
||||
top: 16px;
|
||||
}
|
||||
87.5% {
|
||||
left: 0;
|
||||
top: 8px;
|
||||
}
|
||||
98% {
|
||||
left: 0;
|
||||
top: 8px;
|
||||
}
|
||||
100% {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.rotating-squares-loader {
|
||||
|
||||
@ -18,14 +18,68 @@
|
||||
}
|
||||
|
||||
@keyframes roll {
|
||||
0% { box-shadow: 0 0 transparent, 0 0 transparent, 0 0 transparent, 0 0 transparent; }
|
||||
12% { box-shadow: 6px 0 currentColor, 0 0 transparent, 0 0 transparent, 0 0 transparent; }
|
||||
25% { box-shadow: 12px 0 currentColor, 6px 0 currentColor, 0 0 transparent, 0 0 transparent; }
|
||||
36% { box-shadow: 18px 0 currentColor, 12px 0 currentColor, 6px 0 currentColor, 0 0 transparent; }
|
||||
50% { box-shadow: 24px 0 currentColor, 18px 0 currentColor, 12px 0 currentColor, 6px 0 currentColor; }
|
||||
62% { box-shadow: 36px 0 transparent, 24px 0 currentColor, 18px 0 currentColor, 12px 0 currentColor; }
|
||||
75% { box-shadow: 36px 0 transparent, 36px 0 transparent, 24px 0 currentColor, 18px 0 currentColor; }
|
||||
87% { box-shadow: 36px 0 transparent, 36px 0 transparent, 36px 0 transparent, 24px 0 currentColor; }
|
||||
100% { box-shadow: 36px 0 transparent, 36px 0 transparent, 36px 0 transparent, 36px 0 transparent; }
|
||||
0% {
|
||||
box-shadow:
|
||||
0 0 transparent,
|
||||
0 0 transparent,
|
||||
0 0 transparent,
|
||||
0 0 transparent;
|
||||
}
|
||||
12% {
|
||||
box-shadow:
|
||||
6px 0 currentColor,
|
||||
0 0 transparent,
|
||||
0 0 transparent,
|
||||
0 0 transparent;
|
||||
}
|
||||
25% {
|
||||
box-shadow:
|
||||
12px 0 currentColor,
|
||||
6px 0 currentColor,
|
||||
0 0 transparent,
|
||||
0 0 transparent;
|
||||
}
|
||||
36% {
|
||||
box-shadow:
|
||||
18px 0 currentColor,
|
||||
12px 0 currentColor,
|
||||
6px 0 currentColor,
|
||||
0 0 transparent;
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
24px 0 currentColor,
|
||||
18px 0 currentColor,
|
||||
12px 0 currentColor,
|
||||
6px 0 currentColor;
|
||||
}
|
||||
62% {
|
||||
box-shadow:
|
||||
36px 0 transparent,
|
||||
24px 0 currentColor,
|
||||
18px 0 currentColor,
|
||||
12px 0 currentColor;
|
||||
}
|
||||
75% {
|
||||
box-shadow:
|
||||
36px 0 transparent,
|
||||
36px 0 transparent,
|
||||
24px 0 currentColor,
|
||||
18px 0 currentColor;
|
||||
}
|
||||
87% {
|
||||
box-shadow:
|
||||
36px 0 transparent,
|
||||
36px 0 transparent,
|
||||
36px 0 transparent,
|
||||
24px 0 currentColor;
|
||||
}
|
||||
100% {
|
||||
box-shadow:
|
||||
36px 0 transparent,
|
||||
36px 0 transparent,
|
||||
36px 0 transparent,
|
||||
36px 0 transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -29,13 +29,26 @@
|
||||
animation: wave var(--speed) ease-in-out infinite;
|
||||
}
|
||||
|
||||
.sq:nth-child(1) { animation-delay: calc(var(--speed) * -0.45); }
|
||||
.sq:nth-child(2) { animation-delay: calc(var(--speed) * -0.3); }
|
||||
.sq:nth-child(3) { animation-delay: calc(var(--speed) * -0.15); }
|
||||
.sq:nth-child(4) { animation-delay: 0s; }
|
||||
.sq:nth-child(1) {
|
||||
animation-delay: calc(var(--speed) * -0.45);
|
||||
}
|
||||
.sq:nth-child(2) {
|
||||
animation-delay: calc(var(--speed) * -0.3);
|
||||
}
|
||||
.sq:nth-child(3) {
|
||||
animation-delay: calc(var(--speed) * -0.15);
|
||||
}
|
||||
.sq:nth-child(4) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -29,7 +29,11 @@ export interface MonitorDriver {
|
||||
showThinkingBubble(): void;
|
||||
hideBubble(): void;
|
||||
previewSceneProgress(name: string): void;
|
||||
playScene(name: string, payload: Record<string, any>, runtime: MonitorSceneRuntime): Promise<void>;
|
||||
playScene(
|
||||
name: string,
|
||||
payload: Record<string, any>,
|
||||
runtime: MonitorSceneRuntime
|
||||
): Promise<void>;
|
||||
destroy(): void;
|
||||
preparePendingCreation?(path?: string | null): void;
|
||||
}
|
||||
|
||||
@ -3,7 +3,11 @@
|
||||
<div v-if="node.type === 'folder'" class="file-node folder-node">
|
||||
<button class="folder-header" type="button" :style="folderPadding" @click="toggle">
|
||||
<span class="folder-arrow">{{ isExpanded ? '▾' : '▸' }}</span>
|
||||
<span class="icon icon-sm folder-icon" :style="iconStyle(isExpanded ? 'folderOpen' : 'folder')" aria-hidden="true"></span>
|
||||
<span
|
||||
class="icon icon-sm folder-icon"
|
||||
:style="iconStyle(isExpanded ? 'folderOpen' : 'folder')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="folder-name">{{ node.name }}</span>
|
||||
</button>
|
||||
<div v-show="isExpanded" class="folder-children">
|
||||
|
||||
@ -10,18 +10,43 @@
|
||||
'has-text': (inputMessage || '').trim().length > 0
|
||||
}"
|
||||
>
|
||||
<input type="file" ref="fileUploadInput" class="file-input-hidden" multiple @change="onFileChange" />
|
||||
<input
|
||||
type="file"
|
||||
ref="fileUploadInput"
|
||||
class="file-input-hidden"
|
||||
multiple
|
||||
@change="onFileChange"
|
||||
/>
|
||||
<div class="input-stack">
|
||||
<div v-if="selectedImages && selectedImages.length" class="image-inline-row">
|
||||
<div class="image-thumbnail-wrapper" v-for="img in selectedImages" :key="img">
|
||||
<img :src="getPreviewUrl(img)" :alt="formatImageName(img)" class="image-thumbnail" />
|
||||
<button type="button" class="image-remove-btn-hover" @click.stop="$emit('remove-image', img)">×</button>
|
||||
<button
|
||||
type="button"
|
||||
class="image-remove-btn-hover"
|
||||
@click.stop="$emit('remove-image', img)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedVideos && selectedVideos.length" class="image-inline-row video-inline-row">
|
||||
<div
|
||||
v-if="selectedVideos && selectedVideos.length"
|
||||
class="image-inline-row video-inline-row"
|
||||
>
|
||||
<div class="image-thumbnail-wrapper" v-for="video in selectedVideos" :key="video">
|
||||
<img :src="getPreviewUrl(video)" :alt="formatImageName(video)" class="image-thumbnail" />
|
||||
<button type="button" class="image-remove-btn-hover" @click.stop="$emit('remove-video', video)">×</button>
|
||||
<img
|
||||
:src="getPreviewUrl(video)"
|
||||
:alt="formatImageName(video)"
|
||||
class="image-thumbnail"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="image-remove-btn-hover"
|
||||
@click.stop="$emit('remove-video', video)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
@ -54,7 +79,10 @@
|
||||
!isConnected ||
|
||||
(inputLocked && !streamingMessage) ||
|
||||
(mediaUploading && !streamingMessage) ||
|
||||
((!(inputMessage || '').trim() && (!selectedImages?.length && !selectedVideos?.length)) && !streamingMessage)
|
||||
(!(inputMessage || '').trim() &&
|
||||
!selectedImages?.length &&
|
||||
!selectedVideos?.length &&
|
||||
!streamingMessage)
|
||||
"
|
||||
>
|
||||
<span v-if="streamingMessage" class="stop-icon"></span>
|
||||
@ -165,7 +193,14 @@ const props = defineProps<{
|
||||
currentConversationId: string | null;
|
||||
iconStyle: (key: string) => Record<string, string>;
|
||||
toolCategoryIcon: (categoryId: string) => string;
|
||||
modelOptions: Array<{ key: string; label: string; description: string; disabled?: boolean; supportsImage?: boolean; supportsVideo?: boolean }>;
|
||||
modelOptions: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
disabled?: boolean;
|
||||
supportsImage?: boolean;
|
||||
supportsVideo?: boolean;
|
||||
}>;
|
||||
currentModelKey: string;
|
||||
selectedImages?: string[];
|
||||
selectedVideos?: string[];
|
||||
|
||||
@ -60,7 +60,6 @@
|
||||
<span class="entry-arrow">›</span>
|
||||
</button>
|
||||
|
||||
|
||||
<transition name="submenu-slide">
|
||||
<div class="quick-submenu tool-submenu" v-if="toolMenuOpen">
|
||||
<div class="submenu-status" v-if="toolSettingsLoading">正在同步工具状态...</div>
|
||||
@ -76,13 +75,15 @@
|
||||
:disabled="streamingMessage || !isConnected || toolSettingsLoading"
|
||||
>
|
||||
<span class="submenu-label icon-label">
|
||||
<span class="icon icon-sm" :style="getIconStyle(toolCategoryIcon(category.id))" aria-hidden="true"></span>
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="getIconStyle(toolCategoryIcon(category.id))"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span>{{ category.label }}</span>
|
||||
</span>
|
||||
<span class="entry-arrow">
|
||||
<template v-if="category.locked">
|
||||
被管理员锁定
|
||||
</template>
|
||||
<template v-if="category.locked"> 被管理员锁定 </template>
|
||||
<template v-else>
|
||||
{{ category.enabled ? '禁用' : '启用' }}
|
||||
</template>
|
||||
@ -149,7 +150,14 @@ const props = defineProps<{
|
||||
modeMenuOpen: boolean;
|
||||
runMode?: 'fast' | 'thinking' | 'deep';
|
||||
modelMenuOpen: boolean;
|
||||
modelOptions: Array<{ key: string; label: string; description: string; disabled?: boolean; supportsImage?: boolean; supportsVideo?: boolean }>;
|
||||
modelOptions: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
disabled?: boolean;
|
||||
supportsImage?: boolean;
|
||||
supportsVideo?: boolean;
|
||||
}>;
|
||||
currentModelKey: string;
|
||||
blockUpload?: boolean;
|
||||
blockToolToggle?: boolean;
|
||||
@ -176,41 +184,15 @@ defineEmits<{
|
||||
(event: 'pick-video'): void;
|
||||
}>();
|
||||
|
||||
const runModeOptions = [
|
||||
{ value: 'fast', label: '快速模式' },
|
||||
{ value: 'thinking', label: '思考模式' },
|
||||
{ value: 'deep', label: '深度思考模式' }
|
||||
] as const;
|
||||
|
||||
const runModeLabelMap: Record<'fast' | 'thinking' | 'deep', string> = {
|
||||
fast: '快速模式',
|
||||
thinking: '思考模式',
|
||||
deep: '深度思考'
|
||||
};
|
||||
|
||||
const resolvedRunMode = computed<'fast' | 'thinking' | 'deep'>(() => {
|
||||
if (props.runMode === 'deep' || props.runMode === 'thinking' || props.runMode === 'fast') {
|
||||
return props.runMode;
|
||||
}
|
||||
return props.thinkingMode ? 'thinking' : 'fast';
|
||||
});
|
||||
|
||||
const runModeLabel = computed(() => runModeLabelMap[resolvedRunMode.value]);
|
||||
|
||||
const getIconStyle = (key: string) => (props.iconStyle ? props.iconStyle(key) : {});
|
||||
|
||||
const currentModelLabel = computed(() => {
|
||||
const found = props.modelOptions?.find(m => m.key === props.currentModelKey);
|
||||
return found ? found.label : '未选择';
|
||||
});
|
||||
|
||||
const currentModelSupportsImage = computed(() => {
|
||||
const found = props.modelOptions?.find(m => m.key === props.currentModelKey) as any;
|
||||
const found = props.modelOptions?.find((m) => m.key === props.currentModelKey) as any;
|
||||
return !!found?.supportsImage;
|
||||
});
|
||||
|
||||
const currentModelSupportsVideo = computed(() => {
|
||||
const found = props.modelOptions?.find(m => m.key === props.currentModelKey) as any;
|
||||
const found = props.modelOptions?.find((m) => m.key === props.currentModelKey) as any;
|
||||
return !!found?.supportsVideo;
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -3,16 +3,20 @@
|
||||
<div v-if="activeCommand" class="subagent-activity-overlay" @click.self="close">
|
||||
<div class="subagent-activity-modal bg-command-modal">
|
||||
<div class="subagent-activity-header">
|
||||
<div class="subagent-activity-title">
|
||||
后台指令 {{ activeCommand.command_id }}
|
||||
</div>
|
||||
<div class="subagent-activity-title">后台指令 {{ activeCommand.command_id }}</div>
|
||||
<button type="button" class="subagent-activity-close" @click="close">×</button>
|
||||
</div>
|
||||
<div class="subagent-activity-meta">
|
||||
<span class="subagent-activity-status" :class="activeDetail?.status || activeCommand.status || ''">
|
||||
<span
|
||||
class="subagent-activity-status"
|
||||
:class="activeDetail?.status || activeCommand.status || ''"
|
||||
>
|
||||
{{ activeDetail?.status || activeCommand.status || 'running' }}
|
||||
</span>
|
||||
<span class="subagent-activity-summary" v-if="activeDetail?.command || activeCommand.command">
|
||||
<span
|
||||
class="subagent-activity-summary"
|
||||
v-if="activeDetail?.command || activeCommand.command"
|
||||
>
|
||||
{{ activeDetail?.command || activeCommand.command }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -8,7 +8,9 @@
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div v-if="generatedPath" class="hint">已生成 {{ generatedPath }}</div>
|
||||
<button type="button" class="ghost-btn" @click="$emit('close')" :disabled="submitting">关闭</button>
|
||||
<button type="button" class="ghost-btn" @click="$emit('close')" :disabled="submitting">
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -63,7 +65,9 @@
|
||||
<div class="review-right">
|
||||
<div class="preview-header">
|
||||
<span>预览(前 {{ previewLimit }} 条)</span>
|
||||
<span v-if="preview && preview.length" class="preview-count">{{ preview.length }} 条</span>
|
||||
<span v-if="preview && preview.length" class="preview-count"
|
||||
>{{ preview.length }} 条</span
|
||||
>
|
||||
</div>
|
||||
<div class="preview-box" :class="{ loading: previewLoading }">
|
||||
<div v-if="previewLoading" class="placeholder">
|
||||
@ -90,7 +94,11 @@
|
||||
|
||||
<div class="review-footer">
|
||||
<label class="toggle-send">
|
||||
<input type="checkbox" :checked="sendToModel" @change="$emit('toggle-send', ($event.target as HTMLInputElement).checked)" />
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="sendToModel"
|
||||
@change="$emit('toggle-send', ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<span class="switch"></span>
|
||||
<span class="label">是否发送给模型</span>
|
||||
</label>
|
||||
@ -110,7 +118,7 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'ConversationReviewDialog' });
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
open: boolean;
|
||||
conversations: Array<{
|
||||
id: string;
|
||||
@ -134,9 +142,9 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const icons = {
|
||||
loading: new URL('../../icons/clock.svg', import.meta.url).href,
|
||||
error: new URL('../../icons/triangle-alert.svg', import.meta.url).href,
|
||||
empty: new URL('../../icons/file.svg', import.meta.url).href
|
||||
loading: new URL('../../../icons/clock.svg', import.meta.url).href,
|
||||
error: new URL('../../../icons/triangle-alert.svg', import.meta.url).href,
|
||||
empty: new URL('../../../icons/file.svg', import.meta.url).href
|
||||
};
|
||||
|
||||
defineEmits<{
|
||||
@ -277,7 +285,10 @@ const formatUpdatedAt = (value: string | number) => {
|
||||
border-radius: 10px;
|
||||
padding: 14px 14px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease,
|
||||
transform 0.15s ease;
|
||||
overflow: hidden;
|
||||
min-height: 64px;
|
||||
line-height: 1.4;
|
||||
@ -443,7 +454,10 @@ const formatUpdatedAt = (value: string | number) => {
|
||||
padding: 10px 18px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
|
||||
transition:
|
||||
transform 0.12s ease,
|
||||
box-shadow 0.12s ease,
|
||||
background 0.12s ease;
|
||||
}
|
||||
|
||||
.toggle-send {
|
||||
|
||||
@ -1,16 +1,11 @@
|
||||
<template>
|
||||
<transition name="overlay-fade">
|
||||
<div v-if="open" class="image-picker-backdrop" @click.self="close">
|
||||
<div v-if="open" class="image-picker-backdrop" @click.self="close">
|
||||
<div class="image-picker-panel">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<div class="title">选择图片(最多9张)</div>
|
||||
<button
|
||||
type="button"
|
||||
class="local-btn"
|
||||
:disabled="uploading"
|
||||
@click="triggerLocal"
|
||||
>
|
||||
<button type="button" class="local-btn" :disabled="uploading" @click="triggerLocal">
|
||||
{{ uploading ? '上传中...' : '从本地发送' }}
|
||||
</button>
|
||||
<input
|
||||
@ -25,30 +20,37 @@
|
||||
<button class="close-btn" @click="close">×</button>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<div v-else-if="!images.length" class="empty">未找到图片文件</div>
|
||||
<div v-else class="grid">
|
||||
<div
|
||||
v-for="item in images"
|
||||
:key="item.path"
|
||||
class="card"
|
||||
:class="{ selected: selectedSet.has(item.path) }"
|
||||
@click="toggle(item.path)"
|
||||
:title="item.path"
|
||||
>
|
||||
<img :src="previewUrl(item.path)" :alt="item.name" />
|
||||
<div class="name">{{ item.name }}</div>
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<div v-else-if="!images.length" class="empty">未找到图片文件</div>
|
||||
<div v-else class="grid">
|
||||
<div
|
||||
v-for="item in images"
|
||||
:key="item.path"
|
||||
class="card"
|
||||
:class="{ selected: selectedSet.has(item.path) }"
|
||||
@click="toggle(item.path)"
|
||||
:title="item.path"
|
||||
>
|
||||
<img :src="previewUrl(item.path)" :alt="item.name" />
|
||||
<div class="name">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="count">已选 {{ selectedSet.size }} / 9</div>
|
||||
<div class="actions">
|
||||
<button type="button" class="btn secondary" @click="close">取消</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn primary"
|
||||
:disabled="!selectedSet.size"
|
||||
@click="confirm"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="count">已选 {{ selectedSet.size }} / 9</div>
|
||||
<div class="actions">
|
||||
<button type="button" class="btn secondary" @click="close">取消</button>
|
||||
<button type="button" class="btn primary" :disabled="!selectedSet.size" @click="confirm">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<transition name="overlay-fade">
|
||||
<div v-if="visible" class="new-user-tutorial-mask" @click.self="emitSkip">
|
||||
<div class="new-user-tutorial-dialog" role="dialog" aria-modal="true" aria-label="新手教程提示">
|
||||
<div
|
||||
class="new-user-tutorial-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="新手教程提示"
|
||||
>
|
||||
<p class="eyebrow">欢迎</p>
|
||||
<h3>新用户 {{ username || '用户' }}</h3>
|
||||
<p class="desc">是否需要通过新手教程来快速认识这个系统?</p>
|
||||
@ -87,7 +92,10 @@ button {
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.16s ease, box-shadow 0.2s ease, opacity 0.2s ease;
|
||||
transition:
|
||||
transform 0.16s ease,
|
||||
box-shadow 0.2s ease,
|
||||
opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.primary {
|
||||
|
||||
@ -9,8 +9,12 @@
|
||||
<button type="button" class="subagent-activity-close" @click="close">×</button>
|
||||
</div>
|
||||
<div class="subagent-activity-meta">
|
||||
<span class="subagent-activity-status" :class="activeAgent.status || ''">{{ activeAgent.status || 'running' }}</span>
|
||||
<span class="subagent-activity-summary" v-if="activeAgent.summary">{{ activeAgent.summary }}</span>
|
||||
<span class="subagent-activity-status" :class="activeAgent.status || ''">{{
|
||||
activeAgent.status || 'running'
|
||||
}}</span>
|
||||
<span class="subagent-activity-summary" v-if="activeAgent.summary">{{
|
||||
activeAgent.summary
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="subagent-activity-body">
|
||||
<div v-if="activityError" class="subagent-activity-error">{{ activityError }}</div>
|
||||
|
||||
@ -10,12 +10,37 @@
|
||||
@touchcancel="handleTouchEnd"
|
||||
>
|
||||
<div class="tutorial-click-blocker" @click.stop></div>
|
||||
<div v-if="stepReady && maskVisible" class="tutorial-mask" :style="[maskTopStyle, maskInteractivityStyle]" @click.stop></div>
|
||||
<div v-if="stepReady && maskVisible" class="tutorial-mask" :style="[maskLeftStyle, maskInteractivityStyle]" @click.stop></div>
|
||||
<div v-if="stepReady && maskVisible" class="tutorial-mask" :style="[maskRightStyle, maskInteractivityStyle]" @click.stop></div>
|
||||
<div v-if="stepReady && maskVisible" class="tutorial-mask" :style="[maskBottomStyle, maskInteractivityStyle]" @click.stop></div>
|
||||
<div
|
||||
v-if="stepReady && maskVisible"
|
||||
class="tutorial-mask"
|
||||
:style="[maskTopStyle, maskInteractivityStyle]"
|
||||
@click.stop
|
||||
></div>
|
||||
<div
|
||||
v-if="stepReady && maskVisible"
|
||||
class="tutorial-mask"
|
||||
:style="[maskLeftStyle, maskInteractivityStyle]"
|
||||
@click.stop
|
||||
></div>
|
||||
<div
|
||||
v-if="stepReady && maskVisible"
|
||||
class="tutorial-mask"
|
||||
:style="[maskRightStyle, maskInteractivityStyle]"
|
||||
@click.stop
|
||||
></div>
|
||||
<div
|
||||
v-if="stepReady && maskVisible"
|
||||
class="tutorial-mask"
|
||||
:style="[maskBottomStyle, maskInteractivityStyle]"
|
||||
@click.stop
|
||||
></div>
|
||||
|
||||
<div v-if="stepReady && maskVisible" class="tutorial-highlight" :style="highlightStyle" aria-hidden="true"></div>
|
||||
<div
|
||||
v-if="stepReady && maskVisible"
|
||||
class="tutorial-highlight"
|
||||
:style="highlightStyle"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<span
|
||||
v-if="clickEffect"
|
||||
class="tutorial-click-effect"
|
||||
@ -36,9 +61,13 @@
|
||||
</header>
|
||||
<h3 class="tutorial-popover__title">{{ step?.title || '新手教程' }}</h3>
|
||||
<p class="tutorial-popover__desc">{{ step?.description || '' }}</p>
|
||||
<p v-if="isWaitingTarget" class="tutorial-popover__warn">未找到目标元素,可稍后重试或跳过当前步骤。</p>
|
||||
<p v-if="isWaitingTarget" class="tutorial-popover__warn">
|
||||
未找到目标元素,可稍后重试或跳过当前步骤。
|
||||
</p>
|
||||
<p v-else-if="isMustClick" class="tutorial-popover__hint">请点击高亮目标继续。</p>
|
||||
<p v-if="showPersonalScrollHint" class="tutorial-popover__hint">提示:个人空间内容可上下滚动查看。</p>
|
||||
<p v-if="showPersonalScrollHint" class="tutorial-popover__hint">
|
||||
提示:个人空间内容可上下滚动查看。
|
||||
</p>
|
||||
|
||||
<footer class="tutorial-popover__actions">
|
||||
<button type="button" class="tutorial-btn" :disabled="nextDisabled" @click="goNext">
|
||||
@ -69,7 +98,12 @@ const GAP = 14;
|
||||
const HIGHLIGHT_PADDING = 2;
|
||||
|
||||
const tutorialStore = useTutorialStore();
|
||||
const { running, activeStep: step, visibleStepIndex, totalVisibleSteps } = storeToRefs(tutorialStore);
|
||||
const {
|
||||
running,
|
||||
activeStep: step,
|
||||
visibleStepIndex,
|
||||
totalVisibleSteps
|
||||
} = storeToRefs(tutorialStore);
|
||||
|
||||
const panelRef = ref<HTMLElement | null>(null);
|
||||
const targetRect = ref<RectLike | null>(null);
|
||||
@ -89,7 +123,13 @@ const maskVisible = computed(() => Boolean(targetRect.value));
|
||||
const nextDisabled = computed(() => isMustClick.value && !!targetRect.value);
|
||||
const isPersonalTutorialPhase = computed(() => {
|
||||
const id = step.value?.id || '';
|
||||
return id === 'personal-overview' || id === 'open-personal-space' || id === 'close-personal-space' || id.startsWith('tab-') || id.startsWith('page-');
|
||||
return (
|
||||
id === 'personal-overview' ||
|
||||
id === 'open-personal-space' ||
|
||||
id === 'close-personal-space' ||
|
||||
id.startsWith('tab-') ||
|
||||
id.startsWith('page-')
|
||||
);
|
||||
});
|
||||
const showPersonalScrollHint = computed(() => isPersonalTutorialPhase.value);
|
||||
const nextLabel = computed(() => {
|
||||
@ -117,7 +157,12 @@ const maskTopStyle = computed(() => {
|
||||
const maskLeftStyle = computed(() => {
|
||||
const rect = clampedRect.value;
|
||||
if (!rect) return {};
|
||||
return { top: `${rect.top}px`, left: '0px', width: `${Math.max(0, rect.left)}px`, height: `${rect.height}px` };
|
||||
return {
|
||||
top: `${rect.top}px`,
|
||||
left: '0px',
|
||||
width: `${Math.max(0, rect.left)}px`,
|
||||
height: `${rect.height}px`
|
||||
};
|
||||
});
|
||||
|
||||
const maskRightStyle = computed(() => {
|
||||
@ -228,10 +273,17 @@ const placePopover = (rect: RectLike | null) => {
|
||||
|
||||
const placements: Array<'top' | 'right' | 'bottom' | 'left'> =
|
||||
preferred === 'auto'
|
||||
? (['right', 'bottom', 'left', 'top'] as Array<'top' | 'right' | 'bottom' | 'left'>).sort((a, b) => spaces[b] - spaces[a])
|
||||
? (['right', 'bottom', 'left', 'top'] as Array<'top' | 'right' | 'bottom' | 'left'>).sort(
|
||||
(a, b) => spaces[b] - spaces[a]
|
||||
)
|
||||
: [preferred as 'top' | 'right' | 'bottom' | 'left', 'right', 'bottom', 'left', 'top'];
|
||||
|
||||
let best: { p: 'top' | 'right' | 'bottom' | 'left'; left: number; top: number; overlap: number } | null = null;
|
||||
let best: {
|
||||
p: 'top' | 'right' | 'bottom' | 'left';
|
||||
left: number;
|
||||
top: number;
|
||||
overlap: number;
|
||||
} | null = null;
|
||||
for (const p of Array.from(new Set(placements))) {
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
@ -251,9 +303,19 @@ const placePopover = (rect: RectLike | null) => {
|
||||
|
||||
const clampedLeft = clamp(left, edge, Math.max(edge, vw - panelWidth - edge));
|
||||
const clampedTop = clamp(top, edge, Math.max(edge, vh - panelHeight - edge));
|
||||
const fits = left >= edge && top >= edge && left + panelWidth <= vw - edge && top + panelHeight <= vh - edge;
|
||||
const overlapX = Math.max(0, Math.min(clampedLeft + panelWidth, rect.right) - Math.max(clampedLeft, rect.left));
|
||||
const overlapY = Math.max(0, Math.min(clampedTop + panelHeight, rect.bottom) - Math.max(clampedTop, rect.top));
|
||||
const fits =
|
||||
left >= edge &&
|
||||
top >= edge &&
|
||||
left + panelWidth <= vw - edge &&
|
||||
top + panelHeight <= vh - edge;
|
||||
const overlapX = Math.max(
|
||||
0,
|
||||
Math.min(clampedLeft + panelWidth, rect.right) - Math.max(clampedLeft, rect.left)
|
||||
);
|
||||
const overlapY = Math.max(
|
||||
0,
|
||||
Math.min(clampedTop + panelHeight, rect.bottom) - Math.max(clampedTop, rect.top)
|
||||
);
|
||||
const overlap = overlapX * overlapY;
|
||||
if (fits) {
|
||||
resolvedPlacement.value = p;
|
||||
@ -292,7 +354,14 @@ const updateRect = () => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
targetRect.value =
|
||||
rect.width > 0 && rect.height > 0
|
||||
? { top: rect.top, left: rect.left, width: rect.width, height: rect.height, right: rect.right, bottom: rect.bottom }
|
||||
? {
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom
|
||||
}
|
||||
: null;
|
||||
placePopover(targetRect.value);
|
||||
};
|
||||
@ -321,12 +390,17 @@ const handleCaptureClick = (event: MouseEvent) => {
|
||||
};
|
||||
|
||||
const getPersonalScrollTarget = () => {
|
||||
const pageCandidates = Array.from(document.querySelectorAll('.personalization-content .personal-page')) as HTMLElement[];
|
||||
const pageCandidates = Array.from(
|
||||
document.querySelectorAll('.personalization-content .personal-page')
|
||||
) as HTMLElement[];
|
||||
const pageScrollable = pageCandidates.find((el) => el.scrollHeight - el.clientHeight > 2);
|
||||
if (pageScrollable) return pageScrollable;
|
||||
const content = document.querySelector('.personalization-content') as HTMLElement | null;
|
||||
if (content && content.scrollHeight - content.clientHeight > 2) return content;
|
||||
return (document.querySelector('[data-tutorial="personal-content-shell"]') as HTMLElement | null) || content;
|
||||
return (
|
||||
(document.querySelector('[data-tutorial="personal-content-shell"]') as HTMLElement | null) ||
|
||||
content
|
||||
);
|
||||
};
|
||||
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
@ -397,7 +471,12 @@ const triggerAutoClick = () => {
|
||||
const targetEl = menu || (document.body as HTMLElement);
|
||||
playClickEffect(targetEl);
|
||||
window.setTimeout(() => {
|
||||
const event = new MouseEvent('click', { bubbles: true, cancelable: true, clientX: 12, clientY: 12 });
|
||||
const event = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX: 12,
|
||||
clientY: 12
|
||||
});
|
||||
document.body.dispatchEvent(event);
|
||||
}, 420);
|
||||
return true;
|
||||
@ -420,9 +499,12 @@ const triggerAutoClick = () => {
|
||||
},
|
||||
shouldScroll ? 220 : 0
|
||||
);
|
||||
window.setTimeout(() => {
|
||||
(el as HTMLElement).click();
|
||||
}, shouldScroll ? 640 : 420);
|
||||
window.setTimeout(
|
||||
() => {
|
||||
(el as HTMLElement).click();
|
||||
},
|
||||
shouldScroll ? 640 : 420
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
@ -447,7 +529,7 @@ const handleExit = () => tutorialStore.exitTutorial();
|
||||
|
||||
watch(
|
||||
() => [running.value, step.value?.id],
|
||||
async ([isRunning, stepId], [prevRunning, prevStepId]) => {
|
||||
async ([, stepId], [prevRunning, prevStepId]) => {
|
||||
if (!running.value) {
|
||||
targetRect.value = null;
|
||||
stepReady.value = true;
|
||||
@ -461,7 +543,9 @@ watch(
|
||||
}
|
||||
const nextStep = step.value;
|
||||
const settleDelay =
|
||||
nextStep?.mode === 'must_click' || nextStep?.autoClick || nextStep?.autoOutsideClick ? 500 : 0;
|
||||
nextStep?.mode === 'must_click' || nextStep?.autoClick || nextStep?.autoOutsideClick
|
||||
? 500
|
||||
: 0;
|
||||
stepSettleTimer = window.setTimeout(async () => {
|
||||
await nextTick();
|
||||
updateRect();
|
||||
|
||||
@ -1,16 +1,11 @@
|
||||
<template>
|
||||
<transition name="overlay-fade">
|
||||
<div v-if="open" class="image-picker-backdrop" @click.self="close">
|
||||
<div v-if="open" class="image-picker-backdrop" @click.self="close">
|
||||
<div class="image-picker-panel">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<div class="title">选择视频(一次最多 1 个)</div>
|
||||
<button
|
||||
type="button"
|
||||
class="local-btn"
|
||||
:disabled="uploading"
|
||||
@click="triggerLocal"
|
||||
>
|
||||
<button type="button" class="local-btn" :disabled="uploading" @click="triggerLocal">
|
||||
{{ uploading ? '上传中...' : '从本地发送' }}
|
||||
</button>
|
||||
<input
|
||||
@ -47,7 +42,14 @@
|
||||
<div class="count">已选 {{ selectedSet.size }} / 1</div>
|
||||
<div class="actions">
|
||||
<button type="button" class="btn secondary" @click="close">取消</button>
|
||||
<button type="button" class="btn primary" :disabled="!selectedSet.size" @click="confirm">确认</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn primary"
|
||||
:disabled="!selectedSet.size"
|
||||
@click="confirm"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<aside class="sidebar right-sidebar" :class="{ collapsed }" :style="{ width: collapsed ? '0px' : width + 'px' }">
|
||||
<aside
|
||||
class="sidebar right-sidebar"
|
||||
:class="{ collapsed }"
|
||||
:style="{ width: collapsed ? '0px' : width + 'px' }"
|
||||
>
|
||||
<div class="sidebar-header">
|
||||
<h3 class="icon-label">
|
||||
<span class="icon icon-sm" :style="iconStyle('eye')" aria-hidden="true"></span>
|
||||
|
||||
@ -4,7 +4,11 @@
|
||||
<div class="compact-status-card">
|
||||
<div class="status-line">
|
||||
<div class="status-brand">
|
||||
<span class="status-logo-image status-logo-inline" v-html="statusLogoSvg" aria-hidden="true"></span>
|
||||
<span
|
||||
class="status-logo-image status-logo-inline"
|
||||
v-html="statusLogoSvg"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<div class="brand-text">
|
||||
<span class="brand-name">AI Agent</span>
|
||||
<span class="agent-version" v-if="agentVersion">{{ agentVersion }}</span>
|
||||
@ -42,7 +46,12 @@
|
||||
<div class="sidebar-panel-card">
|
||||
<div class="sidebar-header">
|
||||
<div class="panel-menu-wrapper" ref="panelMenuWrapper">
|
||||
<button class="sidebar-view-toggle" data-tutorial="panel-menu-toggle" @click.stop="$emit('toggle-panel-menu')" title="切换侧边栏">
|
||||
<button
|
||||
class="sidebar-view-toggle"
|
||||
data-tutorial="panel-menu-toggle"
|
||||
@click.stop="$emit('toggle-panel-menu')"
|
||||
title="切换侧边栏"
|
||||
>
|
||||
<span class="icon icon-md" :style="iconStyle('menu')" aria-hidden="true"></span>
|
||||
</button>
|
||||
<transition name="fade">
|
||||
@ -61,7 +70,11 @@
|
||||
@click.stop="$emit('select-panel', 'todo')"
|
||||
title="待办列表"
|
||||
>
|
||||
<span class="icon icon-md" :style="iconStyle('stickyNote')" aria-hidden="true"></span>
|
||||
<span
|
||||
class="icon icon-md"
|
||||
:style="iconStyle('stickyNote')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@ -77,7 +90,11 @@
|
||||
@click.stop="$emit('select-panel', 'backgroundCommands')"
|
||||
title="后台指令"
|
||||
>
|
||||
<span class="icon icon-md" :style="iconStyle('terminal')" aria-hidden="true"></span>
|
||||
<span
|
||||
class="icon icon-md"
|
||||
:style="iconStyle('terminal')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
@ -114,10 +131,19 @@
|
||||
<div v-if="panelMode === 'todo'" class="todo-panel">
|
||||
<div v-if="!todoList" class="todo-empty">暂无待办列表</div>
|
||||
<div v-else>
|
||||
<div class="todo-task" v-for="task in todoList.tasks || []" :key="task.index" :class="{ done: task.status === 'done' }">
|
||||
<div
|
||||
class="todo-task"
|
||||
v-for="task in todoList.tasks || []"
|
||||
:key="task.index"
|
||||
:class="{ done: task.status === 'done' }"
|
||||
>
|
||||
<span class="todo-task-title">task{{ task.index }}:{{ task.title }}</span>
|
||||
<span class="todo-task-status icon-label">
|
||||
<span class="icon icon-sm" :style="iconStyle(task.status === 'done' ? 'check' : 'checkbox')" aria-hidden="true"></span>
|
||||
<span
|
||||
class="icon icon-sm"
|
||||
:style="iconStyle(task.status === 'done' ? 'check' : 'checkbox')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span>{{ task.status === 'done' ? '完成' : '未完成' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
@ -127,7 +153,12 @@
|
||||
<div v-else-if="panelMode === 'subAgents'" class="sub-agent-panel">
|
||||
<div v-if="!subAgents.length" class="sub-agent-empty">暂无运行中的子智能体</div>
|
||||
<div v-else class="sub-agent-cards">
|
||||
<div class="sub-agent-card" v-for="agent in subAgents" :key="agent.task_id" @click="openSubAgent(agent)">
|
||||
<div
|
||||
class="sub-agent-card"
|
||||
v-for="agent in subAgents"
|
||||
:key="agent.task_id"
|
||||
@click="openSubAgent(agent)"
|
||||
>
|
||||
<div class="sub-agent-header">
|
||||
<span class="sub-agent-id">#{{ agent.agent_id }}</span>
|
||||
<span class="sub-agent-status" :class="agent.status">{{ agent.status }}</span>
|
||||
@ -147,10 +178,14 @@
|
||||
@click="openBackgroundCommand(command)"
|
||||
>
|
||||
<div class="sub-agent-header">
|
||||
<span class="sub-agent-id">{{ command.command_id }}</span>
|
||||
<span class="sub-agent-status" :class="command.status">{{ command.status || 'running' }}</span>
|
||||
<span class="sub-agent-id">状态</span>
|
||||
<span class="sub-agent-status" :class="command.status">{{
|
||||
formatBackgroundCommandStatus(command.status)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="sub-agent-summary background-command-summary">
|
||||
{{ formatBackgroundCommandPreview(command.command) }}
|
||||
</div>
|
||||
<div class="sub-agent-summary">{{ command.command || '(空命令)' }}</div>
|
||||
<div class="sub-agent-tool">
|
||||
{{ formatBackgroundCommandMeta(command) }}
|
||||
</div>
|
||||
@ -209,7 +244,7 @@ defineEmits<{
|
||||
}>();
|
||||
|
||||
const panelMenuWrapper = ref<HTMLElement | null>(null);
|
||||
const statusLogoSvg = statusLogoSvgRaw.replace(/fill=\"#000000\"/g, 'fill=\"currentColor\"');
|
||||
const statusLogoSvg = statusLogoSvgRaw.replace(/fill="#000000"/g, 'fill="currentColor"');
|
||||
const panelStyle = computed(() => {
|
||||
if (props.collapsed) {
|
||||
return {
|
||||
@ -266,7 +301,8 @@ const modeIndicatorTitle = computed(() => {
|
||||
const fileStore = useFileStore();
|
||||
const subAgentStore = useSubAgentStore();
|
||||
const backgroundCommandStore = useBackgroundCommandStore();
|
||||
const { fileTree, expandedFolders, todoList, fileTreeUnavailable, fileTreeMessage } = storeToRefs(fileStore);
|
||||
const { fileTree, expandedFolders, todoList, fileTreeUnavailable, fileTreeMessage } =
|
||||
storeToRefs(fileStore);
|
||||
const { subAgents } = storeToRefs(subAgentStore);
|
||||
const { commands: backgroundCommands } = storeToRefs(backgroundCommandStore);
|
||||
|
||||
@ -281,9 +317,22 @@ const openBackgroundCommand = (command: any) => {
|
||||
const formatBackgroundCommandMeta = (command: any) => {
|
||||
const code = command?.return_code;
|
||||
if (code === null || code === undefined) {
|
||||
return '点击查看实时输出';
|
||||
return '状态码:--';
|
||||
}
|
||||
return `返回码:${code}`;
|
||||
return `状态码:${code}`;
|
||||
};
|
||||
|
||||
const formatBackgroundCommandStatus = (status: any) => {
|
||||
const normalized = (status || 'running').toString().toLowerCase();
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const formatBackgroundCommandPreview = (rawCommand: any) => {
|
||||
const content = (rawCommand || '(空命令)').toString().replace(/\s+/g, ' ').trim();
|
||||
if (!content) {
|
||||
return '(空命令)';
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
|
||||
@ -143,7 +143,7 @@ function confirmSelection() {
|
||||
border-radius: 14px;
|
||||
padding: 16px 14px;
|
||||
cursor: pointer;
|
||||
transition: .2s ease;
|
||||
transition: 0.2s ease;
|
||||
box-shadow: 0 8px 18px rgba(61, 57, 41, 0.06);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,11 @@
|
||||
<button type="button" class="confirm-button" @click="resolve(false)">
|
||||
{{ dialog.cancelText || '取消' }}
|
||||
</button>
|
||||
<button type="button" class="confirm-button confirm-button--primary" @click="resolve(true)">
|
||||
<button
|
||||
type="button"
|
||||
class="confirm-button confirm-button--primary"
|
||||
@click="resolve(true)"
|
||||
>
|
||||
{{ dialog.confirmText || '确认' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<div class="easter-egg-overlay" :class="{ active: state.active }" aria-hidden="true" ref="overlayRoot"></div>
|
||||
<div
|
||||
class="easter-egg-overlay"
|
||||
:class="{ active: state.active }"
|
||||
aria-hidden="true"
|
||||
ref="overlayRoot"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@ -2,9 +2,7 @@
|
||||
<transition name="quota-toast-fade">
|
||||
<div class="quota-toast" v-if="quotaToast">
|
||||
<span class="quota-toast-label">{{ quotaToast.message }}</span>
|
||||
<button type="button" class="toast-close" aria-label="关闭通知" @click="dismiss">
|
||||
×
|
||||
</button>
|
||||
<button type="button" class="toast-close" aria-label="关闭通知" @click="dismiss">×</button>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
@ -19,8 +19,18 @@
|
||||
stroke-width="1.7"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M9 9.5h10" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
|
||||
<path d="M9 13h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
|
||||
<path
|
||||
d="M9 9.5h10"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M9 13h6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</slot>
|
||||
</span>
|
||||
@ -62,7 +72,12 @@
|
||||
@click="$emit('toggle-workspace')"
|
||||
>
|
||||
<span class="sr-only">{{ workspaceCollapsed ? '显示工作区' : '隐藏工作区' }}</span>
|
||||
<span class="icon icon-md" data-tutorial="workspace-toggle" aria-hidden="true" :style="iconStyle('layers')"></span>
|
||||
<span
|
||||
class="icon icon-md"
|
||||
data-tutorial="workspace-toggle"
|
||||
aria-hidden="true"
|
||||
:style="iconStyle('layers')"
|
||||
></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@ -72,8 +87,15 @@
|
||||
:aria-disabled="displayModeDisabled"
|
||||
:title="displayMode === 'monitor' ? '退出显示器' : '虚拟显示器'"
|
||||
>
|
||||
<span class="sr-only">{{ displayMode === 'monitor' ? '退出显示器' : '虚拟显示器' }}</span>
|
||||
<span class="icon icon-md" data-tutorial="monitor-toggle" aria-hidden="true" :style="iconStyle('monitor')"></span>
|
||||
<span class="sr-only">{{
|
||||
displayMode === 'monitor' ? '退出显示器' : '虚拟显示器'
|
||||
}}</span>
|
||||
<span
|
||||
class="icon icon-md"
|
||||
data-tutorial="monitor-toggle"
|
||||
aria-hidden="true"
|
||||
:style="iconStyle('monitor')"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@ -85,7 +107,11 @@
|
||||
@click="$emit('create')"
|
||||
>
|
||||
<template v-if="monitorModeActive">
|
||||
<span class="monitor-icon-slot icon icon-md" :style="iconStyle('pencil')" aria-hidden="true"></span>
|
||||
<span
|
||||
class="monitor-icon-slot icon icon-md"
|
||||
:style="iconStyle('pencil')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="sr-only">新建对话</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
@ -111,13 +137,22 @@
|
||||
<span class="sr-only">{{ collapsed ? '展开对话记录' : '折叠对话记录' }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="collapsed" class="icon icon-md" :style="iconStyle('menu')" aria-hidden="true"></span>
|
||||
<span
|
||||
v-if="collapsed"
|
||||
class="icon icon-md"
|
||||
:style="iconStyle('menu')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span v-else class="toggle-arrow" aria-hidden="true">←</span>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="monitorModeActive">
|
||||
<span class="monitor-icon-slot icon icon-md" :style="iconStyle('x')" aria-hidden="true"></span>
|
||||
<span
|
||||
class="monitor-icon-slot icon icon-md"
|
||||
:style="iconStyle('x')"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="sr-only">关闭侧边栏</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
@ -136,13 +171,20 @@
|
||||
:value="searchQuery"
|
||||
placeholder="搜索对话..."
|
||||
@input="$emit('search', ($event.target as HTMLInputElement).value)"
|
||||
@keydown.enter.prevent="$emit('search-submit', ($event.target as HTMLInputElement).value)"
|
||||
@keydown.enter.prevent="
|
||||
$emit('search-submit', ($event.target as HTMLInputElement).value)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="conversation-list">
|
||||
<div v-if="loading && !displayConversations.length && !searchActive" class="loading-conversations">正在加载...</div>
|
||||
<div
|
||||
v-if="loading && !displayConversations.length && !searchActive"
|
||||
class="loading-conversations"
|
||||
>
|
||||
正在加载...
|
||||
</div>
|
||||
<div v-else-if="!displayConversations.length && !loading" class="no-conversations">
|
||||
{{ searchActive ? '未找到匹配对话' : '暂无对话记录' }}
|
||||
</div>
|
||||
@ -158,7 +200,7 @@
|
||||
<div class="conversation-meta">
|
||||
<span class="conversation-time">{{ formatTime(conv.updated_at) }}</span>
|
||||
<span class="conversation-counts">
|
||||
{{ (conv.total_messages || 0) }}条消息
|
||||
{{ conv.total_messages || 0 }}条消息
|
||||
<span v-if="(conv.total_tools || 0) > 0"> · {{ conv.total_tools }}工具</span>
|
||||
</span>
|
||||
</div>
|
||||
@ -183,7 +225,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!searchActive && hasMore" class="load-more">
|
||||
<button class="load-more-btn" type="button" :disabled="loadingMore" @click="$emit('load-more')">
|
||||
<button
|
||||
class="load-more-btn"
|
||||
type="button"
|
||||
:disabled="loadingMore"
|
||||
@click="$emit('load-more')"
|
||||
>
|
||||
{{ loadingMore ? '载入中...' : '加载更多' }}
|
||||
</button>
|
||||
</div>
|
||||
@ -222,7 +269,12 @@
|
||||
@click="$emit('personal')"
|
||||
>
|
||||
<span class="sr-only">个人页面</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
@ -279,7 +331,11 @@ const uiStore = useUiStore();
|
||||
const conversationStore = useConversationStore();
|
||||
const personalizationStore = usePersonalizationStore();
|
||||
|
||||
const { sidebarCollapsed: collapsed, workspaceCollapsed, chatDisplayMode: uiChatDisplayMode } = storeToRefs(uiStore);
|
||||
const {
|
||||
sidebarCollapsed: collapsed,
|
||||
workspaceCollapsed,
|
||||
chatDisplayMode: uiChatDisplayMode
|
||||
} = storeToRefs(uiStore);
|
||||
const {
|
||||
searchQuery,
|
||||
searchResults,
|
||||
@ -299,9 +355,14 @@ const iconStyle = (key: string) => (props.iconStyle ? props.iconStyle(key) : {})
|
||||
const resolvedShowCollapseButton = computed(() => props.showCollapseButton);
|
||||
const resolvedCollapseButtonVariant = computed(() => props.collapseButtonVariant);
|
||||
const displayMode = computed(() => props.displayMode);
|
||||
const monitorModeActive = computed(() => displayMode.value === 'monitor' || uiChatDisplayMode.value === 'monitor');
|
||||
const monitorModeActive = computed(
|
||||
() => displayMode.value === 'monitor' || uiChatDisplayMode.value === 'monitor'
|
||||
);
|
||||
const displayModeDisabled = computed(() => props.displayModeDisabled);
|
||||
const displayConversations = computed(() => (searchActive.value ? searchResults.value : conversations.value));
|
||||
const loading = computed(() => (searchActive.value ? searchInProgress.value : conversationsLoading.value));
|
||||
|
||||
const displayConversations = computed(() =>
|
||||
searchActive.value ? searchResults.value : conversations.value
|
||||
);
|
||||
const loading = computed(() =>
|
||||
searchActive.value ? searchInProgress.value : conversationsLoading.value
|
||||
);
|
||||
</script>
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<div class="token-drawer" v-if="visible" :class="{ collapsed }" data-tutorial="token-drawer">
|
||||
<div class="token-display-panel">
|
||||
<button class="token-close-btn" type="button" data-tutorial="token-close" @click="emit('toggle')" aria-label="收起用量统计">
|
||||
<button
|
||||
class="token-close-btn"
|
||||
type="button"
|
||||
data-tutorial="token-close"
|
||||
@click="emit('toggle')"
|
||||
aria-label="收起用量统计"
|
||||
>
|
||||
<span class="sr-only">关闭</span>
|
||||
</button>
|
||||
<div class="token-panel-content">
|
||||
@ -11,7 +17,9 @@
|
||||
<div class="stat-grid stat-grid--triple">
|
||||
<div class="stat-block">
|
||||
<div class="stat-label">当前上下文</div>
|
||||
<div class="stat-value stat-value--accent">{{ formatTokenCount(currentContextTokens || 0) }}</div>
|
||||
<div class="stat-value stat-value--accent">
|
||||
{{ formatTokenCount(currentContextTokens || 0) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-block">
|
||||
<div class="stat-label">累计输入</div>
|
||||
@ -39,18 +47,20 @@
|
||||
<div class="stat-grid stat-grid--double">
|
||||
<div class="stat-block">
|
||||
<div class="stat-label">CPU</div>
|
||||
<div class="stat-value">{{ formatPercentage(containerStatus.stats.cpu_percent) }}</div>
|
||||
<div class="stat-value">
|
||||
{{ formatPercentage(containerStatus.stats.cpu_percent) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-block">
|
||||
<div class="stat-label">内存</div>
|
||||
<div class="stat-value stat-value--mono">
|
||||
{{ formatBytes(containerStatus.stats.memory.used_bytes) }}
|
||||
<template v-if="containerStatus.stats.memory.limit_bytes">
|
||||
/ {{ formatBytes(containerStatus.stats.memory.limit_bytes) }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="stat-foot" v-if="containerStatus.stats.memory.percent">
|
||||
{{ formatPercentage(containerStatus.stats.memory.percent) }}
|
||||
<div class="stat-block">
|
||||
<div class="stat-label">内存</div>
|
||||
<div class="stat-value stat-value--mono">
|
||||
{{ formatBytes(containerStatus.stats.memory.used_bytes) }}
|
||||
<template v-if="containerStatus.stats.memory.limit_bytes">
|
||||
/ {{ formatBytes(containerStatus.stats.memory.limit_bytes) }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="stat-foot" v-if="containerStatus.stats.memory.percent">
|
||||
{{ formatPercentage(containerStatus.stats.memory.percent) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -65,7 +75,9 @@
|
||||
<div class="stat-block" v-for="tier in quotaTiers" :key="tier.key">
|
||||
<div class="stat-label">{{ tier.label }}</div>
|
||||
<div class="stat-value">{{ formatQuotaValue(tier.value) }}</div>
|
||||
<div class="stat-foot" v-if="(tier.value.count || 0) > 0">重置 {{ formatResetTime(tier.value.reset_at) }}</div>
|
||||
<div class="stat-foot" v-if="(tier.value.count || 0) > 0">
|
||||
重置 {{ formatResetTime(tier.value.reset_at) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -75,14 +87,18 @@
|
||||
<div class="stat-block">
|
||||
<div class="stat-label">网络</div>
|
||||
<div class="stat-value stat-value--mono">
|
||||
↓{{ formatRate(containerNetRate.down_bps) }} ↑{{ formatRate(containerNetRate.up_bps) }}
|
||||
↓{{ formatRate(containerNetRate.down_bps) }} ↑{{
|
||||
formatRate(containerNetRate.up_bps)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-block">
|
||||
<div class="stat-label">存储</div>
|
||||
<div class="stat-value stat-value--mono">
|
||||
{{ formatBytes(projectStorage.used_bytes) }}
|
||||
<template v-if="projectStorage.limit_bytes">/ {{ formatBytes(projectStorage.limit_bytes) }}</template>
|
||||
<template v-if="projectStorage.limit_bytes"
|
||||
>/ {{ formatBytes(projectStorage.limit_bytes) }}</template
|
||||
>
|
||||
</div>
|
||||
<div class="stat-foot" v-if="typeof projectStorage.usage_percent === 'number'">
|
||||
{{ projectStorage.usage_percent.toFixed(1) }}%
|
||||
@ -108,7 +124,10 @@ const emit = defineEmits<{
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
collapsed: boolean;
|
||||
currentConversationTokens: { cumulative_input_tokens?: number; cumulative_output_tokens?: number };
|
||||
currentConversationTokens: {
|
||||
cumulative_input_tokens?: number;
|
||||
cumulative_output_tokens?: number;
|
||||
};
|
||||
currentContextTokens: number;
|
||||
containerStatus: any;
|
||||
containerNetRate: { down_bps: number | null; up_bps: number | null };
|
||||
|
||||
@ -464,6 +464,12 @@
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.background-command-summary {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sub-agent-tool {
|
||||
font-size: 12px;
|
||||
color: var(--claude-text-secondary);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user