1898 lines
81 KiB
Vue
1898 lines
81 KiB
Vue
<template>
|
||
<transition name="personal-page-fade">
|
||
<div
|
||
class="personal-page-overlay"
|
||
v-if="visible"
|
||
@mousedown.self="personalization.handleOverlayPressStart($event)"
|
||
@mouseup.self="personalization.handleOverlayPressEnd"
|
||
@mouseleave.self="personalization.handleOverlayPressCancel"
|
||
@touchstart.self.prevent="personalization.handleOverlayPressStart($event)"
|
||
@touchend.self.prevent="personalization.handleOverlayPressEnd"
|
||
@touchcancel.self="personalization.handleOverlayPressCancel"
|
||
>
|
||
<div class="personal-page-card" data-tutorial="personal-card">
|
||
<div class="personal-page-header">
|
||
<div>
|
||
<h2>个人空间</h2>
|
||
<p>配置 AI 智能体的个性化偏好</p>
|
||
</div>
|
||
<div class="personal-page-actions">
|
||
<button type="button" class="personal-page-logout" @click="personalization.logout()">退出登录</button>
|
||
<button type="button" class="personal-page-close" data-tutorial="personal-close" @click="personalization.closeDrawer()">返回工作区</button>
|
||
</div>
|
||
</div>
|
||
<div class="personalization-body" v-if="!loading">
|
||
<form class="personal-form" @submit.prevent="personalization.save()">
|
||
<div class="personalization-layout">
|
||
<nav class="personal-page-tabs" aria-label="个人空间分组切换">
|
||
<button
|
||
v-for="tab in personalTabs"
|
||
:key="tab.id"
|
||
type="button"
|
||
class="personal-tab-button"
|
||
:data-tutorial="`personal-tab-${tab.id}`"
|
||
:class="{ active: activeTab === tab.id }"
|
||
:aria-pressed="activeTab === tab.id"
|
||
@click.prevent="setActiveTab(tab.id)"
|
||
>
|
||
<span>{{ tab.label }}</span>
|
||
<small v-if="tab.description" class="personal-tab-desc">{{ tab.description }}</small>
|
||
</button>
|
||
</nav>
|
||
<div class="personalization-content-shell" data-tutorial="personal-content-shell">
|
||
<div
|
||
class="personalization-content"
|
||
@touchstart.passive="handleSwipeStart"
|
||
@touchend.passive="handleSwipeEnd"
|
||
>
|
||
<transition name="personal-page-vertical" mode="out-in">
|
||
<section v-if="activeTab === 'preferences'" key="preferences" class="personal-page personal-page-pref" data-tutorial="personal-page-preferences">
|
||
<div class="personal-toggle-row">
|
||
<label class="personal-toggle">
|
||
<span class="toggle-text">
|
||
<span class="toggle-title">启用个性化提示</span>
|
||
<span class="toggle-desc">开启后才会注入您的偏好</span>
|
||
</span>
|
||
<span class="toggle-switch">
|
||
<input
|
||
type="checkbox"
|
||
:checked="form.enabled"
|
||
:disabled="toggleUpdating"
|
||
@change="personalization.toggleEnabled()"
|
||
/>
|
||
<span class="switch-slider"></span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
<p class="section-note">仅作用于当前工作区,可随时在左下角开关。</p>
|
||
<div class="personalization-sections">
|
||
<div class="personal-left-column">
|
||
<div class="personal-section personal-info">
|
||
<label class="personal-field">
|
||
<span>您希望AI智能体怎么自称?</span>
|
||
<input
|
||
type="text"
|
||
placeholder="如:小秘、助理小A"
|
||
:value="form.self_identify"
|
||
maxlength="20"
|
||
@input="personalization.updateField({ key: 'self_identify', value: $event.target.value })"
|
||
@focus="personalization.clearFeedback()"
|
||
/>
|
||
</label>
|
||
<label class="personal-field">
|
||
<span>您希望AI智能体怎么称呼您?</span>
|
||
<input
|
||
type="text"
|
||
placeholder="如:Jojo、老师"
|
||
:value="form.user_name"
|
||
maxlength="20"
|
||
@input="personalization.updateField({ key: 'user_name', value: $event.target.value })"
|
||
@focus="personalization.clearFeedback()"
|
||
/>
|
||
</label>
|
||
<label class="personal-field">
|
||
<span>您的职业是?</span>
|
||
<input
|
||
type="text"
|
||
placeholder="如:产品经理、设计师"
|
||
:value="form.profession"
|
||
maxlength="20"
|
||
@input="personalization.updateField({ key: 'profession', value: $event.target.value })"
|
||
@focus="personalization.clearFeedback()"
|
||
/>
|
||
</label>
|
||
<div class="personal-field">
|
||
<label>
|
||
<span>您希望AI智能体用何种语气与您交流?</span>
|
||
<input
|
||
type="text"
|
||
placeholder="请选择或输入语气"
|
||
:value="form.tone"
|
||
maxlength="20"
|
||
@input="personalization.updateField({ key: 'tone', value: $event.target.value })"
|
||
@focus="personalization.clearFeedback()"
|
||
/>
|
||
</label>
|
||
<div class="tone-preset-row">
|
||
<span>快速填入:</span>
|
||
<div class="tone-preset-buttons">
|
||
<button type="button" v-for="preset in tonePresets" :key="preset" @click.prevent="personalization.applyTonePreset(preset)">
|
||
{{ preset }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="personal-right-column">
|
||
<div class="personal-section personal-considerations">
|
||
<div class="personal-field">
|
||
<span>您希望AI智能体在回答问题时必须考虑的信息是?</span>
|
||
<div class="consideration-input">
|
||
<input
|
||
type="text"
|
||
:value="newConsideration"
|
||
maxlength="50"
|
||
placeholder="输入后点击 + 号添加"
|
||
@input="personalization.updateNewConsideration($event.target.value)"
|
||
@focus="personalization.clearFeedback()"
|
||
/>
|
||
<button
|
||
type="button"
|
||
class="consideration-add"
|
||
:disabled="!newConsideration || form.considerations.length >= maxConsiderations"
|
||
@click="personalization.addConsideration()"
|
||
>
|
||
+
|
||
</button>
|
||
</div>
|
||
<ul class="consideration-list" v-if="form.considerations.length">
|
||
<li
|
||
v-for="(item, idx) in form.considerations"
|
||
:key="`consideration-${idx}`"
|
||
class="consideration-item"
|
||
draggable="true"
|
||
@dragstart="personalization.considerationDragStart(idx, $event)"
|
||
@dragover.prevent="personalization.considerationDragOver(idx, $event)"
|
||
@drop.prevent="personalization.considerationDrop(idx, $event)"
|
||
@dragend="personalization.considerationDragEnd()"
|
||
>
|
||
<span class="drag-handle" aria-hidden="true">≡</span>
|
||
<span class="consideration-text">{{ item }}</span>
|
||
<button type="button" class="consideration-remove" @click="personalization.removeConsideration(idx)">
|
||
−
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
<p class="consideration-hint" v-else>尚未添加任何必备信息</p>
|
||
<p class="consideration-limit">最多 {{ maxConsiderations }} 条,可拖动排序</p>
|
||
</div>
|
||
</div>
|
||
<div class="personal-form-actions card-aligned">
|
||
<div class="personal-status-group">
|
||
<transition name="personal-status-fade">
|
||
<span class="status success" v-if="status">{{ status }}</span>
|
||
</transition>
|
||
<transition name="personal-status-fade">
|
||
<span class="status error" v-if="error">{{ error }}</span>
|
||
</transition>
|
||
</div>
|
||
<button type="submit" class="primary" :disabled="saving">
|
||
{{ saving ? '保存中...' : '保存设置' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section v-else-if="activeTab === 'image'" key="image" class="personal-page behavior-page" data-tutorial="personal-page-image">
|
||
<div class="behavior-section">
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">图片压缩</span>
|
||
<p class="behavior-desc">发送图片或调用 view_image 时自动等比缩放,防止大图占用过多 tokens。</p>
|
||
</div>
|
||
<div class="run-mode-options">
|
||
<button
|
||
v-for="option in imageCompressionOptions"
|
||
:key="option.id"
|
||
type="button"
|
||
class="run-mode-card"
|
||
:class="{ active: form.image_compression === option.id }"
|
||
:aria-pressed="form.image_compression === option.id"
|
||
@click.prevent="personalization.setImageCompression(option.id)"
|
||
>
|
||
<div class="run-mode-card-header">
|
||
<span class="run-mode-title">{{ option.label }}</span>
|
||
</div>
|
||
<p class="run-mode-desc">{{ option.desc }}</p>
|
||
</button>
|
||
</div>
|
||
<p class="behavior-hint">缩放保持原始比例;若原图已低于目标分辨率,将直接使用原图。</p>
|
||
</div>
|
||
</div>
|
||
<div class="personal-actions-row">
|
||
<div class="personal-form-actions card-aligned">
|
||
<div class="personal-status-group">
|
||
<transition name="personal-status-fade">
|
||
<span class="status success" v-if="status">{{ status }}</span>
|
||
</transition>
|
||
<transition name="personal-status-fade">
|
||
<span class="status error" v-if="error">{{ error }}</span>
|
||
</transition>
|
||
</div>
|
||
<button type="submit" class="primary" :disabled="saving">
|
||
{{ saving ? '保存中...' : '保存设置' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section v-else-if="activeTab === 'model'" key="model" class="personal-page behavior-page" data-tutorial="personal-page-model">
|
||
<div class="behavior-section">
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">默认模型</span>
|
||
<p class="field-desc">为新对话/登录后首次请求选择首选模型,会与默认思考模式一起生效。</p>
|
||
</div>
|
||
<div class="run-mode-options model-options">
|
||
<button
|
||
v-for="option in filteredModelOptions"
|
||
:key="option.id"
|
||
type="button"
|
||
class="run-mode-card"
|
||
:class="[{ active: form.default_model === option.value }, { disabled: option.disabled }]"
|
||
:aria-pressed="form.default_model === option.value"
|
||
:disabled="option.disabled"
|
||
@click.prevent="!option.disabled && setDefaultModel(option.value)"
|
||
>
|
||
<div class="run-mode-card-header">
|
||
<span class="run-mode-title">{{ option.label }}</span>
|
||
<span v-if="option.badge" class="run-mode-badge">{{ option.badge }}</span>
|
||
<span v-if="option.disabled" class="run-mode-badge danger">已禁用</span>
|
||
</div>
|
||
<p class="run-mode-desc">{{ option.desc }}</p>
|
||
</button>
|
||
</div>
|
||
<p class="behavior-hint">
|
||
MiniMax-M2.5 仅支持深度思考模式,选择时会给出提示。
|
||
</p>
|
||
</div>
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">默认思考模型</span>
|
||
<p class="field-desc">设定登录或新建任务时的初始运行模式,仍可通过主界面随时切换。</p>
|
||
</div>
|
||
<div class="run-mode-options">
|
||
<button
|
||
v-for="option in runModeOptions"
|
||
:key="option.id"
|
||
type="button"
|
||
class="run-mode-card"
|
||
:class="{ active: isRunModeActive(option.value) }"
|
||
:aria-pressed="isRunModeActive(option.value)"
|
||
@click.prevent="setDefaultRunMode(option.value)"
|
||
>
|
||
<div class="run-mode-card-header">
|
||
<span class="run-mode-title">{{ option.label }}</span>
|
||
<span v-if="option.badge" class="run-mode-badge">{{ option.badge }}</span>
|
||
</div>
|
||
<p class="run-mode-desc">{{ option.desc }}</p>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">思考频率</span>
|
||
<p class="field-desc">控制连续快速模式运行多少轮后,系统会强制切换到思考模型。</p>
|
||
</div>
|
||
<div class="thinking-presets">
|
||
<button
|
||
v-for="preset in thinkingPresets"
|
||
:key="preset.id"
|
||
type="button"
|
||
:class="{ active: isPresetActive(preset.value) }"
|
||
@click.prevent="applyThinkingPreset(preset.value)"
|
||
>
|
||
{{ preset.label }}
|
||
<small>{{ preset.value }} 轮</small>
|
||
</button>
|
||
</div>
|
||
<div class="thinking-input-row">
|
||
<label>
|
||
<span>自定义轮数</span>
|
||
<input
|
||
type="number"
|
||
:min="thinkingIntervalRange.min"
|
||
:max="thinkingIntervalRange.max"
|
||
:placeholder="`默认 ${thinkingIntervalDefault} 轮`"
|
||
:value="form.thinking_interval ?? ''"
|
||
@input="handleThinkingInput"
|
||
@focus="personalization.clearFeedback()"
|
||
/>
|
||
</label>
|
||
<span class="thinking-hint">范围 {{ thinkingIntervalRange.min }}~{{ thinkingIntervalRange.max }} 轮。</span>
|
||
<button type="button" class="link-button" @click.prevent="restoreThinkingInterval">
|
||
恢复默认
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="personal-actions-row">
|
||
<div class="personal-form-actions card-aligned">
|
||
<div class="personal-status-group">
|
||
<transition name="personal-status-fade">
|
||
<span class="status success" v-if="status">{{ status }}</span>
|
||
</transition>
|
||
<transition name="personal-status-fade">
|
||
<span class="status error" v-if="error">{{ error }}</span>
|
||
</transition>
|
||
</div>
|
||
<button type="submit" class="primary" :disabled="saving">
|
||
{{ saving ? '保存中...' : '保存设置' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section v-else-if="activeTab === 'usage'" key="usage" class="personal-page usage-summary-page" data-tutorial="personal-page-usage">
|
||
<div class="usage-summary-card">
|
||
<div class="usage-summary-header">
|
||
<div>
|
||
<p class="usage-summary-eyebrow">对话用量</p>
|
||
<h3>用量统计</h3>
|
||
<p class="usage-summary-desc">累计统计所有对话的输入/输出 Token、对话数量、用户消息与工具调用。</p>
|
||
<p class="usage-summary-note">宿主机模式同样统计 data/conversations 内的记录。</p>
|
||
</div>
|
||
</div>
|
||
<div class="usage-summary-grid usage-summary-grid--tokens">
|
||
<div class="usage-summary-item">
|
||
<div class="label">累计输入</div>
|
||
<div class="value value--success">{{ formatTokenCount(usageSummary.total_input_tokens) }}</div>
|
||
</div>
|
||
<div class="usage-summary-item">
|
||
<div class="label">累计输出</div>
|
||
<div class="value value--warning">{{ formatTokenCount(usageSummary.total_output_tokens) }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="usage-summary-grid usage-summary-grid--counts">
|
||
<div class="usage-summary-item">
|
||
<div class="label">总对话数</div>
|
||
<div class="value">{{ formatTokenCount(usageSummary.total_conversations) }}</div>
|
||
</div>
|
||
<div class="usage-summary-item">
|
||
<div class="label">用户消息数</div>
|
||
<div class="value">{{ formatTokenCount(usageSummary.total_user_messages) }}</div>
|
||
</div>
|
||
<div class="usage-summary-item">
|
||
<div class="label">工具调用次数</div>
|
||
<div class="value">{{ formatTokenCount(usageSummary.total_tools) }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="usage-summary-meta">
|
||
<span v-if="usageError" class="usage-summary-error">{{ usageError }}</span>
|
||
<span v-else-if="usageLoading">正在同步最新统计...</span>
|
||
<span v-else>最近更新:{{ usageUpdatedText }}</span>
|
||
<button
|
||
type="button"
|
||
class="usage-summary-refresh"
|
||
@click="fetchUsageSummary"
|
||
:disabled="usageLoading"
|
||
>
|
||
{{ usageLoading ? '刷新中...' : '刷新数据' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section v-else-if="activeTab === 'app-update'" key="app-update" class="personal-page app-update-page" data-tutorial="personal-page-app-update">
|
||
<div class="usage-summary-card">
|
||
<div class="usage-summary-header">
|
||
<div>
|
||
<p class="usage-summary-eyebrow">App 更新</p>
|
||
<h3>软件更新</h3>
|
||
<p class="usage-summary-desc">仅移动 App 可见。自动检查新版本,也可手动检查。</p>
|
||
<p class="usage-summary-note">
|
||
当前版本:{{ appCurrentVersionText }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="usage-summary-grid usage-summary-grid--counts">
|
||
<div class="usage-summary-item">
|
||
<div class="label">最新版本</div>
|
||
<div class="value">{{ appLatestVersionText }}</div>
|
||
</div>
|
||
<div class="usage-summary-item">
|
||
<div class="label">安装包大小</div>
|
||
<div class="value">{{ appLatestFileSizeText }}</div>
|
||
</div>
|
||
<div class="usage-summary-item">
|
||
<div class="label">更新状态</div>
|
||
<div class="value" :class="{ 'value--warning': appHasUpdate }">
|
||
{{ appUpdateStateText }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="app-update-changelog" v-if="appUpdateChangelogText">
|
||
<div class="app-update-changelog-title">更新说明</div>
|
||
<pre class="app-update-changelog-content">{{ appUpdateChangelogText }}</pre>
|
||
</div>
|
||
|
||
<div class="usage-summary-meta">
|
||
<span v-if="appUpdateError" class="usage-summary-error">{{ appUpdateError }}</span>
|
||
<span v-else-if="appUpdateChecking">正在检查版本...</span>
|
||
<span v-else>{{ appUpdateCheckedText }}</span>
|
||
<div class="app-update-actions">
|
||
<button type="button" class="usage-summary-refresh" @click="checkAppUpdate" :disabled="appUpdateChecking">
|
||
{{ appUpdateChecking ? '检查中...' : '手动检查' }}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="usage-summary-refresh"
|
||
:disabled="!appHasUpdate || !appDownloadUrl"
|
||
@click="downloadLatestApp"
|
||
>
|
||
下载更新
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section v-else-if="activeTab === 'behavior'" key="behavior" class="personal-page behavior-page" data-tutorial="personal-page-behavior">
|
||
<div class="behavior-section">
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">使用自定义称呼</span>
|
||
<p class="field-desc">开启后,对话区域将使用您在"个性化设置"中配置的自称和称呼。关闭则显示默认的"AI Assistant"和"用户"。</p>
|
||
</div>
|
||
<label class="toggle-row">
|
||
<input
|
||
type="checkbox"
|
||
:checked="form.use_custom_names"
|
||
@change="personalization.updateField({ key: 'use_custom_names', value: $event.target.checked })"
|
||
/>
|
||
<span class="fancy-check" aria-hidden="true">
|
||
<svg viewBox="0 0 64 64">
|
||
<path
|
||
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
||
pathLength="575.0541381835938"
|
||
class="fancy-path"
|
||
></path>
|
||
</svg>
|
||
</span>
|
||
<span>在对话中显示自定义称呼</span>
|
||
</label>
|
||
</div>
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">增强工具显示</span>
|
||
<p class="field-desc">开启后工具块将显示格式化的内容,关闭则显示原始 JSON 数据。默认开启。</p>
|
||
</div>
|
||
<label class="toggle-row">
|
||
<input
|
||
type="checkbox"
|
||
:checked="form.enhanced_tool_display"
|
||
@change="personalization.updateField({ key: 'enhanced_tool_display', value: $event.target.checked })"
|
||
/>
|
||
<span class="fancy-check" aria-hidden="true">
|
||
<svg viewBox="0 0 64 64">
|
||
<path
|
||
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
||
pathLength="575.0541381835938"
|
||
class="fancy-path"
|
||
></path>
|
||
</svg>
|
||
</span>
|
||
<span>显示格式化的工具结果(更清晰易读)</span>
|
||
</label>
|
||
</div>
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">上下文压缩策略</span>
|
||
<p class="field-desc">可分别控制自动浅层压缩与自动深层压缩。</p>
|
||
</div>
|
||
<label class="toggle-row">
|
||
<input
|
||
type="checkbox"
|
||
:checked="form.auto_shallow_compress_enabled"
|
||
@change="personalization.updateField({ key: 'auto_shallow_compress_enabled', value: $event.target.checked })"
|
||
/>
|
||
<span class="fancy-check" aria-hidden="true">
|
||
<svg viewBox="0 0 64 64">
|
||
<path
|
||
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
||
pathLength="575.0541381835938"
|
||
class="fancy-path"
|
||
></path>
|
||
</svg>
|
||
</span>
|
||
<span>自动浅层压缩</span>
|
||
</label>
|
||
<label class="toggle-row">
|
||
<input
|
||
type="checkbox"
|
||
:checked="form.auto_deep_compress_enabled"
|
||
@change="personalization.updateField({ key: 'auto_deep_compress_enabled', value: $event.target.checked })"
|
||
/>
|
||
<span class="fancy-check" aria-hidden="true">
|
||
<svg viewBox="0 0 64 64">
|
||
<path
|
||
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
||
pathLength="575.0541381835938"
|
||
class="fancy-path"
|
||
></path>
|
||
</svg>
|
||
</span>
|
||
<span>自动深层压缩</span>
|
||
</label>
|
||
<div class="compression-custom-grid">
|
||
<label class="compression-input-row">
|
||
<span>浅压缩触发上下文</span>
|
||
<input
|
||
type="number"
|
||
min="1000"
|
||
step="1"
|
||
placeholder="默认 80000"
|
||
:value="form.shallow_compress_trigger_tokens ?? ''"
|
||
@input="handleCompressionNumberInput('shallow_compress_trigger_tokens', $event)"
|
||
@focus="personalization.clearFeedback()"
|
||
/>
|
||
</label>
|
||
<label class="compression-input-row">
|
||
<span>浅压缩保留最近 Tool 条数</span>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
step="1"
|
||
placeholder="默认 15"
|
||
:value="form.shallow_compress_keep_recent_tools ?? ''"
|
||
@input="handleCompressionNumberInput('shallow_compress_keep_recent_tools', $event)"
|
||
@focus="personalization.clearFeedback()"
|
||
/>
|
||
</label>
|
||
<label class="compression-input-row">
|
||
<span>浅压缩每轮最多替换条数</span>
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
step="1"
|
||
placeholder="默认 10"
|
||
:value="form.shallow_compress_max_replace_per_round ?? ''"
|
||
@input="handleCompressionNumberInput('shallow_compress_max_replace_per_round', $event)"
|
||
@focus="personalization.clearFeedback()"
|
||
/>
|
||
</label>
|
||
<label class="compression-input-row">
|
||
<span>浅压缩触发间隔(Tool 调用次数)</span>
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
step="1"
|
||
placeholder="默认 10"
|
||
:value="form.shallow_compress_trigger_tool_calls_interval ?? ''"
|
||
@input="handleCompressionNumberInput('shallow_compress_trigger_tool_calls_interval', $event)"
|
||
@focus="personalization.clearFeedback()"
|
||
/>
|
||
</label>
|
||
<label class="compression-input-row">
|
||
<span>深压缩触发上下文</span>
|
||
<input
|
||
type="number"
|
||
min="1000"
|
||
step="1"
|
||
placeholder="默认 150000"
|
||
:value="form.deep_compress_trigger_tokens ?? ''"
|
||
@input="handleCompressionNumberInput('deep_compress_trigger_tokens', $event)"
|
||
@focus="personalization.clearFeedback()"
|
||
/>
|
||
</label>
|
||
</div>
|
||
<div class="compression-actions-row">
|
||
<button type="button" class="compression-reset-btn" @click.prevent="restoreCompressionDefaults">
|
||
恢复默认
|
||
</button>
|
||
</div>
|
||
<p class="field-desc compression-custom-hint">
|
||
自定义项留空则使用默认值;深压缩触发上下文必须大于浅压缩触发上下文。
|
||
</p>
|
||
</div>
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">堆叠块显示模式</span>
|
||
<p class="field-desc">选择思考/工具块的显示方式。默认为堆叠动画。</p>
|
||
</div>
|
||
<div class="run-mode-options">
|
||
<button
|
||
v-for="option in blockDisplayOptions"
|
||
:key="option.id"
|
||
type="button"
|
||
class="run-mode-card"
|
||
:class="{ active: experiments.blockDisplayMode === option.value }"
|
||
:aria-pressed="experiments.blockDisplayMode === option.value"
|
||
@click.prevent="handleBlockDisplayModeChange(option.value)"
|
||
>
|
||
<div class="run-mode-card-header">
|
||
<span class="run-mode-title">{{ option.label }}</span>
|
||
<span v-if="option.badge" class="run-mode-badge">{{ option.badge }}</span>
|
||
</div>
|
||
<p class="run-mode-desc">{{ option.desc }}</p>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">静默禁用</span>
|
||
<p class="field-desc">禁用工具时不再注入提示消息,模型不会感知被禁用项。</p>
|
||
</div>
|
||
<label class="toggle-row">
|
||
<input
|
||
type="checkbox"
|
||
:checked="form.silent_tool_disable"
|
||
@change="personalization.updateField({ key: 'silent_tool_disable', value: $event.target.checked })"
|
||
/>
|
||
<span class="fancy-check" aria-hidden="true">
|
||
<svg viewBox="0 0 64 64">
|
||
<path
|
||
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
||
pathLength="575.0541381835938"
|
||
class="fancy-path"
|
||
></path>
|
||
</svg>
|
||
</span>
|
||
<span>静默禁用工具(不向模型提示)</span>
|
||
</label>
|
||
</div>
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">自动生成对话标题</span>
|
||
<p class="field-desc">默认开启;关闭后标题将沿用首条消息。</p>
|
||
</div>
|
||
<label class="toggle-row">
|
||
<input
|
||
type="checkbox"
|
||
:checked="form.auto_generate_title"
|
||
@change="personalization.updateField({ key: 'auto_generate_title', value: $event.target.checked })"
|
||
/>
|
||
<span class="fancy-check" aria-hidden="true">
|
||
<svg viewBox="0 0 64 64">
|
||
<path
|
||
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
||
pathLength="575.0541381835938"
|
||
class="fancy-path"
|
||
></path>
|
||
</svg>
|
||
</span>
|
||
<span>使用快速模型为新对话生成含 emoji 的简短标题</span>
|
||
</label>
|
||
</div>
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">工具意图提示</span>
|
||
<p class="field-desc">开启后,调用工具时会先用约15字告诉你要做什么,替代“正在/完成”文案。</p>
|
||
</div>
|
||
<label class="toggle-row">
|
||
<input
|
||
type="checkbox"
|
||
:checked="form.tool_intent_enabled"
|
||
@change="personalization.updateField({ key: 'tool_intent_enabled', value: $event.target.checked })"
|
||
/>
|
||
<span class="fancy-check" aria-hidden="true">
|
||
<svg viewBox="0 0 64 64">
|
||
<path
|
||
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
||
pathLength="575.0541381835938"
|
||
class="fancy-path"
|
||
></path>
|
||
</svg>
|
||
</span>
|
||
<span>在工具块显示"我要做什么"的简短提示</span>
|
||
</label>
|
||
</div>
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">Skill 提示系统</span>
|
||
<p class="field-desc">开启后,检测到特定关键词时会自动提示模型阅读相关 skill(如 terminal-guide、sub-agent-guide 等)。</p>
|
||
</div>
|
||
<label class="toggle-row">
|
||
<input
|
||
type="checkbox"
|
||
:checked="form.skill_hints_enabled"
|
||
@change="personalization.updateField({ key: 'skill_hints_enabled', value: $event.target.checked })"
|
||
/>
|
||
<span class="fancy-check" aria-hidden="true">
|
||
<svg viewBox="0 0 64 64">
|
||
<path
|
||
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
||
pathLength="575.0541381835938"
|
||
class="fancy-path"
|
||
></path>
|
||
</svg>
|
||
</span>
|
||
<span>根据关键词自动提示阅读相关 skill</span>
|
||
</label>
|
||
</div>
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">强约束系统</span>
|
||
<p class="field-desc">开启后,若未在当前对话先阅读对应 SKILL.md,直接调用工具会返回“必须先阅读”。仅对个人空间中已启用的 skill 生效。</p>
|
||
</div>
|
||
<div class="strict-skill-grid">
|
||
<label class="toggle-row">
|
||
<input
|
||
type="checkbox"
|
||
:checked="form.skill_strict_terminal_enabled"
|
||
@change="personalization.updateField({ key: 'skill_strict_terminal_enabled', value: $event.target.checked })"
|
||
/>
|
||
<span class="fancy-check" aria-hidden="true">
|
||
<svg viewBox="0 0 64 64">
|
||
<path
|
||
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
||
pathLength="575.0541381835938"
|
||
class="fancy-path"
|
||
></path>
|
||
</svg>
|
||
</span>
|
||
<span>Terminal 系列工具</span>
|
||
</label>
|
||
<label class="toggle-row">
|
||
<input
|
||
type="checkbox"
|
||
:checked="form.skill_strict_sub_agent_enabled"
|
||
@change="personalization.updateField({ key: 'skill_strict_sub_agent_enabled', value: $event.target.checked })"
|
||
/>
|
||
<span class="fancy-check" aria-hidden="true">
|
||
<svg viewBox="0 0 64 64">
|
||
<path
|
||
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
||
pathLength="575.0541381835938"
|
||
class="fancy-path"
|
||
></path>
|
||
</svg>
|
||
</span>
|
||
<span>子智能体系列工具</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">默认禁用工具类别</span>
|
||
<p class="field-desc">选择后,这些类别在新任务中会保持关闭,也可随时通过快捷菜单临时开启。</p>
|
||
</div>
|
||
<div class="tool-category-grid" v-if="toolCategories.length">
|
||
<label
|
||
v-for="category in toolCategories"
|
||
:key="category.id"
|
||
class="tool-category-chip"
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
:checked="form.disabled_tool_categories.includes(category.id)"
|
||
@change="toggleCategory(category.id)"
|
||
/>
|
||
<span>{{ category.label }}</span>
|
||
</label>
|
||
</div>
|
||
<p class="behavior-hint" v-else>暂无可配置的工具类别。</p>
|
||
</div>
|
||
</div>
|
||
<div class="personal-actions-row">
|
||
<div class="personal-form-actions card-aligned">
|
||
<div class="personal-status-group">
|
||
<transition name="personal-status-fade">
|
||
<span class="status success" v-if="status">{{ status }}</span>
|
||
</transition>
|
||
<transition name="personal-status-fade">
|
||
<span class="status error" v-if="error">{{ error }}</span>
|
||
</transition>
|
||
</div>
|
||
<button type="submit" class="primary" :disabled="saving">
|
||
{{ saving ? '保存中...' : '保存设置' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section v-else-if="activeTab === 'skills'" key="skills" class="personal-page behavior-page" data-tutorial="personal-page-skills">
|
||
<div class="behavior-section">
|
||
<div class="behavior-field">
|
||
<div class="behavior-field-header">
|
||
<span class="field-title">可用 Skills</span>
|
||
<p class="field-desc">勾选后会注入 system prompt,并同步到工作区的 skills/ 目录。</p>
|
||
</div>
|
||
<div class="tool-category-grid" v-if="skillsCatalog.length">
|
||
<label
|
||
v-for="skill in skillsCatalog"
|
||
:key="skill.id"
|
||
class="tool-category-chip"
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
:checked="form.enabled_skills.includes(skill.id)"
|
||
@change="personalization.toggleSkill(skill.id)"
|
||
/>
|
||
<span>{{ skill.label }}</span>
|
||
</label>
|
||
</div>
|
||
<p class="behavior-hint" v-else>暂无可用的 skills。</p>
|
||
</div>
|
||
</div>
|
||
<div class="personal-actions-row">
|
||
<div class="personal-form-actions card-aligned">
|
||
<div class="personal-status-group">
|
||
<transition name="personal-status-fade">
|
||
<span class="status success" v-if="status">{{ status }}</span>
|
||
</transition>
|
||
<transition name="personal-status-fade">
|
||
<span class="status error" v-if="error">{{ error }}</span>
|
||
</transition>
|
||
</div>
|
||
<button type="submit" class="primary" :disabled="saving">
|
||
{{ saving ? '保存中...' : '保存设置' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section v-else-if="activeTab === 'theme'" key="theme" class="personal-page theme-page" data-tutorial="personal-page-theme">
|
||
<div class="theme-section">
|
||
<div class="theme-header">
|
||
<div>
|
||
<h3>界面主题</h3>
|
||
<p>在浅色、深色和 Claude 经典之间一键切换。会立即应用并保存在本地。</p>
|
||
</div>
|
||
<div class="theme-badge">Beta</div>
|
||
</div>
|
||
<div class="theme-grid">
|
||
<button
|
||
v-for="option in themeOptions"
|
||
:key="option.id"
|
||
type="button"
|
||
class="theme-card"
|
||
:class="{ active: activeTheme === option.id }"
|
||
@click.prevent="applyThemeOption(option.id)"
|
||
>
|
||
<div class="theme-card-head">
|
||
<div class="theme-dot-row">
|
||
<span
|
||
v-for="(color, idx) in option.swatches"
|
||
:key="`${option.id}-${idx}`"
|
||
class="theme-dot"
|
||
:style="{ background: color }"
|
||
aria-hidden="true"
|
||
></span>
|
||
</div>
|
||
<span class="theme-check" v-if="activeTheme === option.id">✓</span>
|
||
</div>
|
||
<div class="theme-card-body">
|
||
<h4>{{ option.label }}</h4>
|
||
<p>{{ option.desc }}</p>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
<p class="theme-note">虚拟显示器保持原样;仅主界面、侧边栏与个人空间配色随主题改变。</p>
|
||
</div>
|
||
</section>
|
||
<section v-else-if="activeTab === 'tutorial'" key="tutorial" class="personal-page tutorial-page">
|
||
<div class="tutorial-intro-content">
|
||
<div class="tutorial-intro-header">
|
||
<p class="tutorial-intro-eyebrow">功能引导</p>
|
||
<h3>新手教程</h3>
|
||
<p class="tutorial-intro-desc">
|
||
通过高亮 + 悬浮说明窗,快速了解主要功能入口和常用设置。
|
||
</p>
|
||
</div>
|
||
|
||
<div class="tutorial-intro-features">
|
||
<div class="tutorial-feature-item">
|
||
<div>
|
||
<h4>主界面导览</h4>
|
||
<p>认识侧边栏、工作区、模型与模式选择。</p>
|
||
</div>
|
||
</div>
|
||
<div class="tutorial-feature-item">
|
||
<div>
|
||
<h4>快捷菜单</h4>
|
||
<p>学习上传文件、对话回顾、用量统计等高频功能。</p>
|
||
</div>
|
||
</div>
|
||
<div class="tutorial-feature-item">
|
||
<div>
|
||
<h4>个人空间</h4>
|
||
<p>了解模型偏好、主题、Skills、图片压缩等配置。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tutorial-intro-actions">
|
||
<button type="button" class="tutorial-start-button" @click="startTutorial">
|
||
开始新手教程
|
||
</button>
|
||
<p class="tutorial-intro-hint">教程开始后会自动关闭个人空间,您可随时退出或跳过当前步骤。</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section v-else-if="activeTab === 'admin-monitor'" key="admin-monitor" class="personal-page admin-monitor-page">
|
||
<div class="admin-monitor-panel">
|
||
<div class="admin-monitor-heading">
|
||
<p class="admin-monitor-eyebrow">系统资源</p>
|
||
<div class="admin-monitor-title-row">
|
||
<h3>管理员监控入口</h3>
|
||
<span class="admin-monitor-chip">自动刷新</span>
|
||
</div>
|
||
<p class="admin-monitor-desc">独立面板集中展示配额趋势、容器运行、项目存储与上传安全。</p>
|
||
</div>
|
||
<div class="admin-monitor-highlights">
|
||
<span class="admin-monitor-tag">用量统计</span>
|
||
<span class="admin-monitor-tag">容器健康</span>
|
||
<span class="admin-monitor-tag">上传审计</span>
|
||
<span class="admin-monitor-tag">邀请码管理</span>
|
||
</div>
|
||
<div class="admin-monitor-actions">
|
||
<button type="button" class="admin-monitor-button" @click="openAdminPanel">
|
||
打开监控面板
|
||
</button>
|
||
<button type="button" class="admin-monitor-button ghost" @click="openApiAdmin">
|
||
API 管理
|
||
</button>
|
||
<p class="admin-monitor-hint">新窗口开启,不打断当前对话与配置。</p>
|
||
</div>
|
||
</div>
|
||
<div class="admin-monitor-panel secondary">
|
||
<div class="admin-monitor-heading">
|
||
<p class="admin-monitor-eyebrow">自定义工具</p>
|
||
<div class="admin-monitor-title-row">
|
||
<h3>自定义工具管理</h3>
|
||
<span class="admin-monitor-chip alt">低代码 · Python</span>
|
||
</div>
|
||
<p class="admin-monitor-desc">在线创建/编辑 definition.json、execution.py、return.json、meta.json,保存后自动生效。</p>
|
||
</div>
|
||
<div class="admin-monitor-highlights">
|
||
<span class="admin-monitor-tag">仅管理员可见</span>
|
||
<span class="admin-monitor-tag">容器内执行</span>
|
||
<span class="admin-monitor-tag">无需重启</span>
|
||
<span class="admin-monitor-tag">三层拆分</span>
|
||
</div>
|
||
<div class="admin-monitor-actions">
|
||
<button type="button" class="admin-monitor-button" @click="openCustomTools">
|
||
打开自定义工具
|
||
</button>
|
||
<p class="admin-monitor-hint">新窗口打开 /admin/custom-tools 页面。</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</transition>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="personalization-loading" v-else>正在加载个性化配置...</div>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, onMounted } from 'vue';
|
||
import { storeToRefs } from 'pinia';
|
||
import { usePersonalizationStore, type BlockDisplayMode } from '@/stores/personalization';
|
||
import { useResourceStore } from '@/stores/resource';
|
||
import { useUiStore } from '@/stores/ui';
|
||
import { usePolicyStore } from '@/stores/policy';
|
||
import { useTutorialStore } from '@/stores/tutorial';
|
||
import { formatTokenCount } from '@/utils/formatters';
|
||
import { useTheme } from '@/utils/theme';
|
||
import type { ThemeKey } from '@/utils/theme';
|
||
|
||
defineOptions({ name: 'PersonalizationDrawer' });
|
||
|
||
const personalization = usePersonalizationStore();
|
||
const resourceStore = useResourceStore();
|
||
const uiStore = useUiStore();
|
||
const tutorialStore = useTutorialStore();
|
||
const {
|
||
visible,
|
||
loading,
|
||
form,
|
||
tonePresets,
|
||
newConsideration,
|
||
maxConsiderations,
|
||
status,
|
||
error,
|
||
saving,
|
||
toggleUpdating,
|
||
toolCategories,
|
||
skillsCatalog,
|
||
thinkingIntervalDefault,
|
||
thinkingIntervalRange,
|
||
experiments
|
||
} = storeToRefs(personalization);
|
||
|
||
const baseTabs = [
|
||
{ id: 'preferences', label: '个性化设置', description: '称呼、语气与注意事项' },
|
||
{ id: 'model', label: '模型偏好', description: '默认模型选择' },
|
||
{ id: 'usage', label: '用量统计', description: '对话累计数据' },
|
||
{ id: 'app-update', label: '软件更新', description: '检查并下载新版 APK' },
|
||
{ id: 'behavior', label: '模型行为', description: '工具提示与界面表现' },
|
||
{ id: 'skills', label: 'Skills', description: '可用技能开关' },
|
||
{ id: 'image', label: '图片压缩', description: '发送图片的尺寸策略' },
|
||
{ id: 'theme', label: '主题切换', description: '浅色 / 深色 / Claude' },
|
||
{ id: 'tutorial', label: '新手教程', description: '功能引导与使用说明' }
|
||
] as const;
|
||
|
||
type PersonalTab =
|
||
| 'preferences'
|
||
| 'model'
|
||
| 'usage'
|
||
| 'app-update'
|
||
| 'behavior'
|
||
| 'skills'
|
||
| 'image'
|
||
| 'theme'
|
||
| 'tutorial'
|
||
| 'admin-monitor';
|
||
|
||
const isAdmin = computed(() => (resourceStore.usageQuota.role || '').toLowerCase() === 'admin');
|
||
const isAppShell = computed(() => {
|
||
if (typeof window === 'undefined') return false;
|
||
const params = new URLSearchParams(window.location.search);
|
||
if (params.has('app_shell')) return true;
|
||
return Boolean((window as any)?.AndroidThemeBridge);
|
||
});
|
||
|
||
const personalTabs = computed(() => {
|
||
const tabs = baseTabs.filter((tab) => (tab.id === 'app-update' ? isAppShell.value : true));
|
||
if (isAdmin.value) {
|
||
tabs.push({ id: 'admin-monitor' as const, label: '管理员监控', description: '系统总览' });
|
||
}
|
||
return tabs;
|
||
});
|
||
|
||
const activeTab = ref<PersonalTab>('preferences');
|
||
const swipeState = ref<{ startY: number; active: boolean }>({ startY: 0, active: false });
|
||
|
||
type RunModeValue = 'fast' | 'thinking' | 'deep' | null;
|
||
type CompressionField =
|
||
| 'shallow_compress_trigger_tokens'
|
||
| 'shallow_compress_keep_recent_tools'
|
||
| 'shallow_compress_max_replace_per_round'
|
||
| 'shallow_compress_trigger_tool_calls_interval'
|
||
| 'deep_compress_trigger_tokens';
|
||
|
||
const runModeOptions: Array<{ id: string; label: string; desc: string; value: RunModeValue; badge?: string }> = [
|
||
{ id: 'fast', label: '快速模式', desc: '追求响应速度,跳过思考模型', value: 'fast' },
|
||
{ id: 'thinking', label: '思考模式', desc: '首轮回复会先输出思考过程', value: 'thinking', badge: '推荐' },
|
||
{ id: 'deep', label: '深度思考', desc: '整轮对话都使用思考模型', value: 'deep' }
|
||
];
|
||
|
||
const policyStore = usePolicyStore();
|
||
|
||
const modelOptions = [
|
||
{ id: 'deepseek', label: 'DeepSeek', desc: '通用 + 思考强化', value: 'deepseek' },
|
||
{ id: 'kimi-k2.5', label: 'Kimi-k2.5', desc: '新版 Kimi,思考开关 + 图文多模态', value: 'kimi-k2.5', badge: '图文' },
|
||
{ id: 'kimi', label: 'Kimi-k2', desc: '旧版 Kimi-k2,兼顾通用对话', value: 'kimi' },
|
||
{ id: 'qwen3-vl-plus', label: 'Qwen3.5', desc: '图文多模态 + 深度思考', value: 'qwen3-vl-plus', badge: '图文' },
|
||
{ id: 'minimax-m2.5', label: 'MiniMax-M2.5', desc: '仅深度思考,超长上下文', value: 'minimax-m2.5', badge: '深度思考' }
|
||
] as const;
|
||
|
||
const filteredModelOptions = computed(() =>
|
||
modelOptions.map((opt) => ({
|
||
...opt,
|
||
disabled: policyStore.disabledModelSet.has(opt.value)
|
||
}))
|
||
);
|
||
|
||
const thinkingPresets = [
|
||
{ id: 'low', label: '低', value: 10 },
|
||
{ id: 'medium', label: '中', value: 5 },
|
||
{ id: 'high', label: '高', value: 3 }
|
||
];
|
||
|
||
const imageCompressionOptions = [
|
||
{ id: 'original', label: '原图', desc: '不压缩' },
|
||
{ id: '1080p', label: '1080p', desc: '最长边不超过 1080p 等比缩放' },
|
||
{ id: '720p', label: '720p', desc: '最长边不超过 720p 等比缩放' },
|
||
{ id: '540p', label: '540p', desc: '最长边不超过 540p 等比缩放' }
|
||
] as const;
|
||
|
||
const usageSummary = ref({
|
||
total_input_tokens: 0,
|
||
total_output_tokens: 0,
|
||
total_conversations: 0,
|
||
total_user_messages: 0,
|
||
total_tools: 0
|
||
});
|
||
const usageLoading = ref(false);
|
||
const usageError = ref('');
|
||
const usageUpdatedAt = ref<string | null>(null);
|
||
|
||
const usageUpdatedText = computed(() => {
|
||
if (!usageUpdatedAt.value) {
|
||
return '尚未刷新';
|
||
}
|
||
const date = new Date(usageUpdatedAt.value);
|
||
if (Number.isNaN(date.getTime())) {
|
||
return '时间未知';
|
||
}
|
||
return date.toLocaleString('zh-CN', {
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
});
|
||
|
||
const appCurrentVersionCode = ref<number | null>(null);
|
||
const appCurrentVersionName = ref<string>('');
|
||
const appUpdateChecking = ref(false);
|
||
const appUpdateError = ref('');
|
||
const appUpdateCheckedAt = ref<string | null>(null);
|
||
const appUpdateInfo = ref<{
|
||
latestVersionCode: number;
|
||
latestVersionName: string;
|
||
apkUrl: string;
|
||
fileSizeBytes: number;
|
||
changelog?: string;
|
||
publishedAt?: string | null;
|
||
hasUpdate?: boolean | null;
|
||
} | null>(null);
|
||
|
||
if (typeof window !== 'undefined') {
|
||
const params = new URLSearchParams(window.location.search);
|
||
const vcRaw = params.get('app_vc') || '';
|
||
const vnRaw = params.get('app_vn') || '';
|
||
const bridge = (window as any)?.AndroidThemeBridge;
|
||
const bridgeVcRaw = bridge && typeof bridge.getAppVersionCode === 'function' ? String(bridge.getAppVersionCode() || '') : '';
|
||
const bridgeVnRaw = bridge && typeof bridge.getAppVersionName === 'function' ? String(bridge.getAppVersionName() || '') : '';
|
||
// App 端优先使用 Bridge 实时版本,避免 WebView 恢复旧 URL 参数导致显示过期版本号
|
||
const finalVcRaw = bridgeVcRaw || vcRaw;
|
||
const finalVnRaw = bridgeVnRaw || vnRaw;
|
||
appCurrentVersionCode.value = /^\d+$/.test(finalVcRaw) ? Number(finalVcRaw) : null;
|
||
appCurrentVersionName.value = finalVnRaw || '';
|
||
}
|
||
|
||
const hydrateAppVersionFromBridge = () => {
|
||
if (typeof window === 'undefined') return;
|
||
const bridge = (window as any)?.AndroidThemeBridge;
|
||
if (!bridge) return;
|
||
try {
|
||
const vcRaw = typeof bridge.getAppVersionCode === 'function' ? String(bridge.getAppVersionCode() || '') : '';
|
||
const vnRaw = typeof bridge.getAppVersionName === 'function' ? String(bridge.getAppVersionName() || '') : '';
|
||
if (vcRaw && /^\d+$/.test(vcRaw)) {
|
||
appCurrentVersionCode.value = Number(vcRaw);
|
||
}
|
||
if (vnRaw) {
|
||
appCurrentVersionName.value = vnRaw;
|
||
}
|
||
} catch {
|
||
// ignore bridge read failures
|
||
}
|
||
};
|
||
|
||
const appCurrentVersionText = computed(() => {
|
||
const vn = appCurrentVersionName.value || '未知';
|
||
const vc = appCurrentVersionCode.value ?? '?';
|
||
return `${vn} (${vc})`;
|
||
});
|
||
|
||
const appLatestVersionText = computed(() => {
|
||
if (!appUpdateInfo.value) return '--';
|
||
return `${appUpdateInfo.value.latestVersionName || '未知'} (${appUpdateInfo.value.latestVersionCode ?? '?'})`;
|
||
});
|
||
|
||
const appLatestFileSizeText = computed(() => {
|
||
const bytes = Number(appUpdateInfo.value?.fileSizeBytes || 0);
|
||
if (!bytes) return '--';
|
||
const mb = bytes / 1024 / 1024;
|
||
return `${mb.toFixed(mb >= 100 ? 0 : 1)} MB`;
|
||
});
|
||
|
||
const appUpdateChangelogText = computed(() => {
|
||
const text = (appUpdateInfo.value?.changelog || '').trim();
|
||
return text || '';
|
||
});
|
||
|
||
const appHasUpdate = computed(() => {
|
||
if (!appUpdateInfo.value) return false;
|
||
if (typeof appUpdateInfo.value.hasUpdate === 'boolean') return appUpdateInfo.value.hasUpdate;
|
||
if (appCurrentVersionCode.value == null) return false;
|
||
return Number(appUpdateInfo.value.latestVersionCode || 0) > appCurrentVersionCode.value;
|
||
});
|
||
|
||
const appDownloadUrl = computed(() => appUpdateInfo.value?.apkUrl || '');
|
||
|
||
const appUpdateStateText = computed(() => {
|
||
if (!appUpdateInfo.value) return '未检查';
|
||
return appHasUpdate.value ? '发现新版本' : '已是最新';
|
||
});
|
||
|
||
const appUpdateCheckedText = computed(() => {
|
||
if (!appUpdateCheckedAt.value) return '尚未检查更新';
|
||
const d = new Date(appUpdateCheckedAt.value);
|
||
if (Number.isNaN(d.getTime())) return '刚刚检查过';
|
||
return `最近检查:${d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`;
|
||
});
|
||
|
||
const fetchUsageSummary = async () => {
|
||
if (usageLoading.value) {
|
||
return;
|
||
}
|
||
usageLoading.value = true;
|
||
usageError.value = '';
|
||
try {
|
||
const response = await fetch('/api/conversations/statistics');
|
||
const payload = await response.json();
|
||
if (!response.ok || !payload.success) {
|
||
throw new Error(payload.error || payload.message || '获取用量统计失败');
|
||
}
|
||
const data = payload.data || {};
|
||
const tokenStats = data.token_statistics || {};
|
||
usageSummary.value = {
|
||
total_input_tokens: Number(tokenStats.total_input_tokens || 0),
|
||
total_output_tokens: Number(tokenStats.total_output_tokens || 0),
|
||
total_conversations: Number(data.total_conversations || 0),
|
||
total_user_messages: Number(data.total_user_messages || 0),
|
||
total_tools: Number(data.total_tools || 0)
|
||
};
|
||
usageUpdatedAt.value = new Date().toISOString();
|
||
} catch (error: any) {
|
||
usageError.value = error?.message || '获取用量统计失败';
|
||
} finally {
|
||
usageLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const checkAppUpdate = async () => {
|
||
if (appUpdateChecking.value) return;
|
||
hydrateAppVersionFromBridge();
|
||
appUpdateChecking.value = true;
|
||
appUpdateError.value = '';
|
||
try {
|
||
const params = new URLSearchParams();
|
||
if (appCurrentVersionCode.value != null) {
|
||
params.set('currentVersionCode', String(appCurrentVersionCode.value));
|
||
}
|
||
const suffix = params.toString() ? `?${params.toString()}` : '';
|
||
const resp = await fetch(`/api/app/version${suffix}`);
|
||
const payload = await resp.json();
|
||
if (!resp.ok || !payload?.success) {
|
||
throw new Error(payload?.error || '检查更新失败');
|
||
}
|
||
const data = payload?.data || {};
|
||
appUpdateInfo.value = {
|
||
latestVersionCode: Number(data.latestVersionCode || 0),
|
||
latestVersionName: String(data.latestVersionName || ''),
|
||
apkUrl: String(data.apkUrl || ''),
|
||
fileSizeBytes: Number(data.fileSizeBytes || 0),
|
||
changelog: String(data.changelog || ''),
|
||
publishedAt: data.publishedAt || null,
|
||
hasUpdate: typeof data.hasUpdate === 'boolean' ? data.hasUpdate : null
|
||
};
|
||
appUpdateCheckedAt.value = new Date().toISOString();
|
||
} catch (error: any) {
|
||
appUpdateError.value = error?.message || '检查更新失败';
|
||
} finally {
|
||
appUpdateChecking.value = false;
|
||
}
|
||
};
|
||
|
||
const downloadLatestApp = () => {
|
||
const url = appDownloadUrl.value;
|
||
if (!url) {
|
||
appUpdateError.value = '未拿到下载地址';
|
||
return;
|
||
}
|
||
window.location.href = url;
|
||
};
|
||
|
||
onMounted(() => {
|
||
hydrateAppVersionFromBridge();
|
||
});
|
||
|
||
watch(
|
||
() => [activeTab.value, visible.value],
|
||
([tab, isVisible]) => {
|
||
if (isVisible && tab === 'usage') {
|
||
fetchUsageSummary();
|
||
}
|
||
if (isVisible && tab === 'app-update' && isAppShell.value && !appUpdateInfo.value && !appUpdateChecking.value) {
|
||
checkAppUpdate();
|
||
}
|
||
}
|
||
);
|
||
|
||
const setActiveTab = (tab: PersonalTab) => {
|
||
activeTab.value = tab;
|
||
};
|
||
|
||
const startTutorial = () => {
|
||
personalization.closeDrawer();
|
||
window.setTimeout(() => {
|
||
tutorialStore.startTutorial();
|
||
}, 320);
|
||
};
|
||
|
||
const shiftTab = (direction: 1 | -1) => {
|
||
const tabs = personalTabs.value;
|
||
const currentIndex = tabs.findIndex((tab) => tab.id === activeTab.value);
|
||
if (currentIndex === -1) {
|
||
return;
|
||
}
|
||
const nextIndex = currentIndex + direction;
|
||
if (nextIndex < 0 || nextIndex >= tabs.length) {
|
||
return;
|
||
}
|
||
activeTab.value = tabs[nextIndex].id as PersonalTab;
|
||
};
|
||
|
||
const handleSwipeStart = (event: TouchEvent) => {
|
||
// 手机端禁用滑动切换标签页
|
||
if (window.innerWidth <= 768) {
|
||
return;
|
||
}
|
||
if (!event.touches.length) {
|
||
return;
|
||
}
|
||
swipeState.value = { startY: event.touches[0].clientY, active: true };
|
||
};
|
||
|
||
const handleSwipeEnd = (event: TouchEvent) => {
|
||
// 手机端禁用滑动切换标签页
|
||
if (window.innerWidth <= 768) {
|
||
return;
|
||
}
|
||
if (!swipeState.value.active || !event.changedTouches.length) {
|
||
return;
|
||
}
|
||
const deltaY = event.changedTouches[0].clientY - swipeState.value.startY;
|
||
swipeState.value.active = false;
|
||
if (Math.abs(deltaY) < 60) {
|
||
return;
|
||
}
|
||
shiftTab(deltaY < 0 ? 1 : -1);
|
||
};
|
||
|
||
const applyThinkingPreset = (value: number) => {
|
||
personalization.setThinkingInterval(value);
|
||
};
|
||
|
||
const isRunModeActive = (value: RunModeValue) => {
|
||
if (value === null) {
|
||
return !form.value.default_run_mode;
|
||
}
|
||
return form.value.default_run_mode === value;
|
||
};
|
||
|
||
const setDefaultRunMode = (value: RunModeValue) => {
|
||
if (checkModeModelConflict(value, form.value.default_model)) {
|
||
return;
|
||
}
|
||
personalization.setDefaultRunMode(value);
|
||
};
|
||
|
||
const setDefaultModel = (value: string) => {
|
||
if (policyStore.disabledModelSet.has(value)) {
|
||
uiStore.pushToast({
|
||
title: '模型被禁用',
|
||
message: '已被管理员禁用,无法选择',
|
||
type: 'warning'
|
||
});
|
||
return;
|
||
}
|
||
if (checkModeModelConflict(form.value.default_run_mode, value)) {
|
||
return;
|
||
}
|
||
personalization.setDefaultModel(value);
|
||
};
|
||
|
||
const checkModeModelConflict = (mode: RunModeValue, model: string | null): boolean => {
|
||
const warnings: string[] = [];
|
||
if (model === 'minimax-m2.5' && mode && mode !== 'deep') {
|
||
warnings.push('MiniMax-M2.5 仅支持深度思考模式,已保持原设置。');
|
||
}
|
||
if (warnings.length) {
|
||
uiStore.pushToast({
|
||
title: '模型/思考模式不兼容',
|
||
message: warnings.join(' '),
|
||
type: 'warning',
|
||
duration: 6000
|
||
});
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
const handleThinkingInput = (event: Event) => {
|
||
const target = event.target as HTMLInputElement;
|
||
if (!target.value) {
|
||
personalization.setThinkingInterval(null);
|
||
return;
|
||
}
|
||
const parsed = Number(target.value);
|
||
personalization.setThinkingInterval(Number.isNaN(parsed) ? null : parsed);
|
||
};
|
||
|
||
const restoreThinkingInterval = () => {
|
||
personalization.setThinkingInterval(null);
|
||
};
|
||
|
||
const handleCompressionNumberInput = (key: CompressionField, event: Event) => {
|
||
const target = event.target as HTMLInputElement | null;
|
||
if (!target || !target.value) {
|
||
personalization.updateField({ key, value: null });
|
||
return;
|
||
}
|
||
const parsed = Number(target.value);
|
||
if (!Number.isFinite(parsed)) {
|
||
personalization.updateField({ key, value: null });
|
||
return;
|
||
}
|
||
personalization.updateField({ key, value: Math.round(parsed) });
|
||
};
|
||
|
||
const restoreCompressionDefaults = () => {
|
||
personalization.updateField({ key: 'shallow_compress_trigger_tokens', value: null });
|
||
personalization.updateField({ key: 'shallow_compress_keep_recent_tools', value: null });
|
||
personalization.updateField({ key: 'shallow_compress_max_replace_per_round', value: null });
|
||
personalization.updateField({ key: 'shallow_compress_trigger_tool_calls_interval', value: null });
|
||
personalization.updateField({ key: 'deep_compress_trigger_tokens', value: null });
|
||
};
|
||
|
||
const isPresetActive = (value: number) => {
|
||
if (form.value.thinking_interval === null || typeof form.value.thinking_interval === 'undefined') {
|
||
return value === thinkingIntervalDefault.value;
|
||
}
|
||
return form.value.thinking_interval === value;
|
||
};
|
||
|
||
const toggleCategory = (categoryId: string) => {
|
||
personalization.toggleDefaultToolCategory(categoryId);
|
||
};
|
||
|
||
const blockDisplayOptions = [
|
||
{ id: 'traditional', label: '传统列表', desc: '按时间顺序垂直排列,经典样式', value: 'traditional' as const },
|
||
{ id: 'stacked', label: '堆叠动画', desc: '超过 6 条自动收纳为"更多",动态展示', value: 'stacked' as const, badge: '推荐' },
|
||
{ id: 'minimal', label: '极简模式', desc: '摘要行 + 无缝展开,流式输出效果', value: 'minimal' as const, badge: '新' }
|
||
];
|
||
|
||
const handleBlockDisplayModeChange = (mode: 'traditional' | 'stacked' | 'minimal') => {
|
||
personalization.setBlockDisplayMode(mode);
|
||
};
|
||
|
||
const openAdminPanel = () => {
|
||
window.open('/admin/monitor', '_blank', 'noopener');
|
||
personalization.closeDrawer();
|
||
};
|
||
|
||
const openCustomTools = () => {
|
||
window.open('/admin/custom-tools', '_blank', 'noopener');
|
||
personalization.closeDrawer();
|
||
};
|
||
|
||
const openApiAdmin = () => {
|
||
window.open('/admin/api', '_blank', 'noopener');
|
||
personalization.closeDrawer();
|
||
};
|
||
|
||
// ===== 主题切换 =====
|
||
const { setTheme, loadTheme } = useTheme();
|
||
const themeOptions: Array<{ id: ThemeKey; label: string; desc: string; swatches: string[] }> = [
|
||
{
|
||
id: 'claude',
|
||
label: 'Claude 经典',
|
||
desc: '仿 Claude 的米色质感,柔和高对比',
|
||
swatches: ['#eeece2', '#f7f3ea', '#da7756']
|
||
},
|
||
{
|
||
id: 'light',
|
||
label: '明亮',
|
||
desc: '纯白底色 + 优雅灰,类似 ChatGPT 风格',
|
||
swatches: ['#ffffff', '#f7f7f8', '#6b7280']
|
||
},
|
||
{
|
||
id: 'dark',
|
||
label: '夜间',
|
||
desc: '深灰 + 黑,低亮度并保持彩色点缀',
|
||
swatches: ['#1a1a1a', '#2a2a2a', '#3a3a3a']
|
||
}
|
||
];
|
||
|
||
const activeTheme = ref<ThemeKey>(loadTheme());
|
||
|
||
const applyThemeOption = (theme: ThemeKey) => {
|
||
activeTheme.value = theme;
|
||
setTheme(theme);
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.compression-custom-grid {
|
||
margin-top: 10px;
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
|
||
.compression-input-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
color: var(--claude-text-secondary);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.compression-input-row input {
|
||
border: 1px solid var(--claude-border);
|
||
border-radius: 10px;
|
||
background: var(--claude-panel);
|
||
color: var(--claude-text);
|
||
padding: 8px 10px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.compression-custom-hint {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.compression-actions-row {
|
||
margin-top: 4px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.compression-reset-btn {
|
||
border: 1px solid var(--claude-border);
|
||
border-radius: 10px;
|
||
padding: 6px 12px;
|
||
background: var(--claude-panel);
|
||
color: var(--claude-text);
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||
}
|
||
|
||
.compression-reset-btn:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 8px 18px rgba(61, 57, 41, 0.14);
|
||
}
|
||
|
||
.usage-summary-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
padding: 12px 0 24px;
|
||
height: 100%;
|
||
}
|
||
|
||
.usage-summary-card {
|
||
width: 100%;
|
||
max-width: 720px;
|
||
padding: 0;
|
||
min-height: 320px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18px;
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
}
|
||
|
||
.usage-summary-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.usage-summary-header h3 {
|
||
margin: 4px 0 6px;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.usage-summary-eyebrow {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
color: var(--claude-accent-strong);
|
||
letter-spacing: 0.18em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.usage-summary-desc {
|
||
margin: 0;
|
||
color: var(--claude-text-secondary);
|
||
}
|
||
|
||
.usage-summary-note {
|
||
margin: 6px 0 0;
|
||
font-size: 12px;
|
||
color: var(--claude-text-tertiary);
|
||
}
|
||
|
||
.usage-summary-refresh {
|
||
border: 1px solid var(--claude-border);
|
||
border-radius: 12px;
|
||
padding: 6px 14px;
|
||
background: var(--claude-panel);
|
||
color: var(--claude-text);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||
box-shadow: 0 6px 14px rgba(61, 57, 41, 0.12);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.usage-summary-refresh:disabled {
|
||
opacity: 0.7;
|
||
cursor: not-allowed;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.usage-summary-refresh:not(:disabled):hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 14px 28px rgba(61, 57, 41, 0.22);
|
||
}
|
||
|
||
.app-update-page .app-update-actions {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.app-update-changelog {
|
||
border: 1px solid var(--claude-border);
|
||
border-radius: 14px;
|
||
padding: 12px;
|
||
background: var(--claude-panel);
|
||
max-height: 300px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.app-update-changelog-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--claude-text);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.app-update-changelog-content {
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
font-size: 12px;
|
||
line-height: 1.55;
|
||
color: var(--claude-text-secondary);
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
max-height: 240px;
|
||
overflow: auto;
|
||
}
|
||
|
||
.usage-summary-grid {
|
||
display: grid;
|
||
gap: 14px;
|
||
}
|
||
|
||
.usage-summary-grid--tokens {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.usage-summary-grid--counts {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
|
||
.usage-summary-item {
|
||
padding: 14px 16px;
|
||
border-radius: 16px;
|
||
border: 1px solid var(--claude-border);
|
||
background: transparent;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.usage-summary-item .label {
|
||
font-size: 12px;
|
||
color: var(--claude-text-secondary);
|
||
}
|
||
|
||
.usage-summary-item .value {
|
||
font-size: 22px;
|
||
font-weight: 600;
|
||
color: var(--claude-text);
|
||
}
|
||
|
||
.usage-summary-item .value--success {
|
||
color: var(--claude-success);
|
||
}
|
||
|
||
.usage-summary-item .value--warning {
|
||
color: var(--claude-warning);
|
||
}
|
||
|
||
.usage-summary-item .value--accent {
|
||
color: var(--claude-text);
|
||
}
|
||
|
||
.usage-summary-meta {
|
||
font-size: 12px;
|
||
color: var(--claude-text-secondary);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
margin-top: auto;
|
||
}
|
||
|
||
.usage-summary-error {
|
||
color: var(--claude-warning);
|
||
}
|
||
|
||
.admin-monitor-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
align-items: center;
|
||
min-height: 320px;
|
||
padding: 12px 0 24px;
|
||
}
|
||
|
||
.admin-monitor-panel {
|
||
width: 100%;
|
||
max-width: 640px;
|
||
padding: 28px 32px;
|
||
border-radius: 26px;
|
||
border: 1px solid var(--theme-control-border, var(--claude-border));
|
||
background: linear-gradient(145deg, var(--theme-surface-soft), var(--theme-surface-muted));
|
||
box-shadow: var(--theme-shadow-soft, var(--claude-shadow));
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18px;
|
||
}
|
||
|
||
.admin-monitor-panel.secondary {
|
||
background: linear-gradient(145deg, var(--theme-surface-muted), var(--theme-surface-soft));
|
||
}
|
||
|
||
.admin-monitor-heading {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.admin-monitor-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.admin-monitor-title-row h3 {
|
||
margin: 0;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.admin-monitor-desc {
|
||
margin: 0;
|
||
color: var(--claude-text-secondary);
|
||
max-width: 460px;
|
||
}
|
||
|
||
.admin-monitor-chip {
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--theme-chip-border, var(--claude-border));
|
||
font-size: 12px;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--claude-text-secondary);
|
||
background: var(--theme-chip-bg, var(--claude-panel));
|
||
}
|
||
|
||
.admin-monitor-eyebrow {
|
||
font-size: 12px;
|
||
color: var(--claude-accent-strong);
|
||
letter-spacing: 0.18em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.admin-monitor-highlights {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.admin-monitor-tag {
|
||
padding: 6px 14px;
|
||
border-radius: 999px;
|
||
border: 1px dashed var(--theme-chip-border, var(--claude-border));
|
||
background: var(--theme-chip-bg, var(--claude-panel));
|
||
font-size: 13px;
|
||
color: var(--claude-text-secondary);
|
||
}
|
||
|
||
.section-note {
|
||
margin: 6px 0 14px;
|
||
font-size: 12px;
|
||
color: var(--claude-text-secondary);
|
||
}
|
||
|
||
.admin-monitor-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
}
|
||
|
||
.admin-monitor-button {
|
||
border: none;
|
||
border-radius: 16px;
|
||
padding: 11px 28px;
|
||
background: linear-gradient(135deg, var(--claude-accent), var(--claude-accent-strong));
|
||
color: #fff;
|
||
font-weight: 600;
|
||
letter-spacing: 0.04em;
|
||
box-shadow: var(--theme-shadow-soft, var(--claude-shadow));
|
||
cursor: pointer;
|
||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
}
|
||
.admin-monitor-button.ghost {
|
||
background: transparent;
|
||
color: var(--claude-text);
|
||
border: 1px dashed var(--theme-chip-border, var(--claude-border));
|
||
box-shadow: none;
|
||
}
|
||
|
||
.admin-monitor-button:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: var(--theme-shadow-mid, var(--claude-shadow));
|
||
}
|
||
|
||
.admin-monitor-button:active {
|
||
transform: translateY(0);
|
||
box-shadow: var(--theme-shadow-soft, var(--claude-shadow));
|
||
}
|
||
|
||
.admin-monitor-hint {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
color: var(--claude-text-secondary);
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.usage-summary-refresh {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
.usage-summary-grid--tokens,
|
||
.usage-summary-grid--counts {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.admin-monitor-panel {
|
||
padding: 24px;
|
||
}
|
||
|
||
.admin-monitor-actions {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.admin-monitor-button {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
.admin-monitor-hint {
|
||
text-align: center;
|
||
}
|
||
}
|
||
</style>
|