新增了一个全新的消息显示模式,提供更简洁的对话体验。 主要功能: - 新增 MinimalBlocks.vue 组件,实现极简风格的消息块展示 - 在 ChatArea.vue 中集成极简模式渲染逻辑 - 在个性化设置中添加"极简模式"选项(blockDisplayMode) - 支持三种显示模式:堆叠模式、极简模式、传统模式 技术实现: - 使用 personalization store 管理显示模式状态 - 通过 computed 属性动态切换渲染组件 - 保持与现有堆叠模式和传统模式的兼容性 用户体验: - 更清爽的界面布局 - 减少视觉干扰 - 适合快速浏览对话内容 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
562 lines
18 KiB
TypeScript
562 lines
18 KiB
TypeScript
import { defineStore } from 'pinia';
|
||
|
||
export type BlockDisplayMode = 'traditional' | 'stacked' | 'minimal';
|
||
type RunMode = 'fast' | 'thinking' | 'deep';
|
||
|
||
interface PersonalForm {
|
||
enabled: boolean;
|
||
auto_generate_title: boolean;
|
||
tool_intent_enabled: boolean;
|
||
skill_hints_enabled: boolean;
|
||
silent_tool_disable: boolean;
|
||
enhanced_tool_display: boolean;
|
||
enabled_skills: string[];
|
||
self_identify: string;
|
||
user_name: string;
|
||
use_custom_names: boolean;
|
||
profession: string;
|
||
tone: string;
|
||
considerations: string[];
|
||
thinking_interval: number | null;
|
||
disabled_tool_categories: string[];
|
||
default_run_mode: RunMode | null;
|
||
default_model: string | null;
|
||
image_compression: string;
|
||
}
|
||
|
||
interface LiquidGlassPosition {
|
||
left: number;
|
||
top: number;
|
||
}
|
||
|
||
interface ExperimentState {
|
||
liquidGlassEnabled: boolean;
|
||
liquidGlassPosition: LiquidGlassPosition | null;
|
||
blockDisplayMode: BlockDisplayMode;
|
||
}
|
||
|
||
interface PersonalizationState {
|
||
visible: boolean;
|
||
loading: boolean;
|
||
saving: boolean;
|
||
loaded: boolean;
|
||
status: string;
|
||
error: string;
|
||
maxConsiderations: number;
|
||
toggleUpdating: boolean;
|
||
overlayPressActive: boolean;
|
||
newConsideration: string;
|
||
tonePresets: string[];
|
||
draggedConsiderationIndex: number | null;
|
||
form: PersonalForm;
|
||
toolCategories: Array<{ id: string; label: string }>;
|
||
skillsCatalog: Array<{ id: string; label: string; description?: string }>;
|
||
thinkingIntervalDefault: number;
|
||
thinkingIntervalRange: { min: number; max: number };
|
||
experiments: ExperimentState;
|
||
}
|
||
|
||
const DEFAULT_INTERVAL = 10;
|
||
const DEFAULT_INTERVAL_RANGE = { min: 1, max: 50 };
|
||
const RUN_MODE_OPTIONS: RunMode[] = ['fast', 'thinking', 'deep'];
|
||
const EXPERIMENT_STORAGE_KEY = 'agents_personalization_experiments';
|
||
|
||
const defaultForm = (): PersonalForm => ({
|
||
enabled: false,
|
||
auto_generate_title: true,
|
||
tool_intent_enabled: true,
|
||
skill_hints_enabled: false,
|
||
silent_tool_disable: false,
|
||
enhanced_tool_display: true,
|
||
enabled_skills: [],
|
||
self_identify: '',
|
||
user_name: '',
|
||
use_custom_names: false,
|
||
profession: '',
|
||
tone: '',
|
||
considerations: [],
|
||
thinking_interval: null,
|
||
disabled_tool_categories: [],
|
||
default_run_mode: null,
|
||
default_model: 'kimi-k2.5',
|
||
image_compression: 'original'
|
||
});
|
||
|
||
const defaultExperimentState = (): ExperimentState => ({
|
||
liquidGlassEnabled: false,
|
||
liquidGlassPosition: null,
|
||
blockDisplayMode: 'stacked'
|
||
});
|
||
|
||
const isValidPosition = (value: any): value is LiquidGlassPosition => {
|
||
return (
|
||
value &&
|
||
typeof value === 'object' &&
|
||
typeof value.left === 'number' &&
|
||
typeof value.top === 'number' &&
|
||
Number.isFinite(value.left) &&
|
||
Number.isFinite(value.top)
|
||
);
|
||
};
|
||
|
||
const loadExperimentState = (): ExperimentState => {
|
||
if (typeof window === 'undefined' || !window.localStorage) {
|
||
return defaultExperimentState();
|
||
}
|
||
try {
|
||
const raw = window.localStorage.getItem(EXPERIMENT_STORAGE_KEY);
|
||
if (!raw) {
|
||
return defaultExperimentState();
|
||
}
|
||
const parsed = JSON.parse(raw);
|
||
|
||
// 兼容旧版 stackedBlocksEnabled
|
||
let blockDisplayMode: BlockDisplayMode = defaultExperimentState().blockDisplayMode;
|
||
if (typeof parsed?.blockDisplayMode === 'string' &&
|
||
['traditional', 'stacked', 'minimal'].includes(parsed.blockDisplayMode)) {
|
||
blockDisplayMode = parsed.blockDisplayMode;
|
||
} else if (typeof parsed?.stackedBlocksEnabled === 'boolean') {
|
||
// 兼容旧版:true -> stacked, false -> traditional
|
||
blockDisplayMode = parsed.stackedBlocksEnabled ? 'stacked' : 'traditional';
|
||
}
|
||
|
||
return {
|
||
liquidGlassEnabled: Boolean(parsed?.liquidGlassEnabled),
|
||
liquidGlassPosition: isValidPosition(parsed?.liquidGlassPosition) ? parsed?.liquidGlassPosition : null,
|
||
blockDisplayMode
|
||
};
|
||
} catch (error) {
|
||
console.warn('无法读取实验功能设置:', error);
|
||
return defaultExperimentState();
|
||
}
|
||
};
|
||
|
||
export const usePersonalizationStore = defineStore('personalization', {
|
||
state: (): PersonalizationState => ({
|
||
visible: false,
|
||
loading: false,
|
||
saving: false,
|
||
loaded: false,
|
||
status: '',
|
||
error: '',
|
||
maxConsiderations: 10,
|
||
toggleUpdating: false,
|
||
overlayPressActive: false,
|
||
newConsideration: '',
|
||
tonePresets: ['健谈', '幽默', '直言不讳', '鼓励性', '诗意', '企业商务', '打破常规', '同理心'],
|
||
draggedConsiderationIndex: null,
|
||
form: defaultForm(),
|
||
toolCategories: [],
|
||
skillsCatalog: [],
|
||
thinkingIntervalDefault: DEFAULT_INTERVAL,
|
||
thinkingIntervalRange: { ...DEFAULT_INTERVAL_RANGE },
|
||
experiments: loadExperimentState()
|
||
}),
|
||
actions: {
|
||
async openDrawer() {
|
||
this.visible = true;
|
||
if (!this.loaded && !this.loading) {
|
||
await this.fetchPersonalization();
|
||
}
|
||
},
|
||
closeDrawer() {
|
||
this.visible = false;
|
||
this.draggedConsiderationIndex = null;
|
||
this.overlayPressActive = false;
|
||
},
|
||
handleOverlayPressStart(event: Event) {
|
||
if (event && (event as MouseEvent).type === 'mousedown') {
|
||
const mouse = event as MouseEvent;
|
||
if (mouse.button !== 0) {
|
||
return;
|
||
}
|
||
}
|
||
this.overlayPressActive = true;
|
||
},
|
||
handleOverlayPressEnd() {
|
||
if (!this.overlayPressActive) {
|
||
return;
|
||
}
|
||
this.overlayPressActive = false;
|
||
this.closeDrawer();
|
||
},
|
||
handleOverlayPressCancel() {
|
||
this.overlayPressActive = false;
|
||
},
|
||
async fetchPersonalization() {
|
||
this.loading = true;
|
||
this.error = '';
|
||
try {
|
||
const resp = await fetch('/api/personalization');
|
||
const result = await resp.json();
|
||
if (!resp.ok || !result.success) {
|
||
throw new Error(result.error || '加载失败');
|
||
}
|
||
this.applyPersonalizationData(result.data || {});
|
||
this.applyPersonalizationMeta(result);
|
||
this.loaded = true;
|
||
} catch (error: any) {
|
||
this.error = error?.message || '加载失败';
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
applyPersonalizationData(data: any) {
|
||
// 若后端未返回默认模型(旧版本接口),保持当前已选模型而不是回退为 Kimi
|
||
const fallbackModel =
|
||
(this.form && typeof this.form.default_model === 'string' ? this.form.default_model : null) || 'kimi-k2.5';
|
||
this.form = {
|
||
enabled: !!data.enabled,
|
||
auto_generate_title: data.auto_generate_title !== false,
|
||
tool_intent_enabled: !!data.tool_intent_enabled,
|
||
skill_hints_enabled: !!data.skill_hints_enabled,
|
||
silent_tool_disable: !!data.silent_tool_disable,
|
||
enhanced_tool_display: data.enhanced_tool_display !== false,
|
||
enabled_skills: Array.isArray(data.enabled_skills)
|
||
? data.enabled_skills.filter((item: unknown) => typeof item === 'string')
|
||
: [],
|
||
self_identify: data.self_identify || '',
|
||
user_name: data.user_name || '',
|
||
use_custom_names: !!data.use_custom_names,
|
||
profession: data.profession || '',
|
||
tone: data.tone || '',
|
||
considerations: Array.isArray(data.considerations) ? [...data.considerations] : [],
|
||
thinking_interval: typeof data.thinking_interval === 'number' ? data.thinking_interval : null,
|
||
disabled_tool_categories: Array.isArray(data.disabled_tool_categories) ? data.disabled_tool_categories.filter((item: unknown) => typeof item === 'string') : [],
|
||
default_run_mode:
|
||
typeof data.default_run_mode === 'string' && RUN_MODE_OPTIONS.includes(data.default_run_mode as RunMode)
|
||
? data.default_run_mode as RunMode
|
||
: null,
|
||
default_model: typeof data.default_model === 'string' ? data.default_model : fallbackModel,
|
||
image_compression: typeof data.image_compression === 'string' ? data.image_compression : 'original'
|
||
};
|
||
this.clearFeedback();
|
||
},
|
||
applyPersonalizationMeta(payload: any) {
|
||
if (payload && typeof payload.thinking_interval_default === 'number') {
|
||
this.thinkingIntervalDefault = payload.thinking_interval_default;
|
||
} else {
|
||
this.thinkingIntervalDefault = DEFAULT_INTERVAL;
|
||
}
|
||
if (payload && payload.thinking_interval_range) {
|
||
const { min, max } = payload.thinking_interval_range;
|
||
this.thinkingIntervalRange = {
|
||
min: typeof min === 'number' ? min : DEFAULT_INTERVAL_RANGE.min,
|
||
max: typeof max === 'number' ? max : DEFAULT_INTERVAL_RANGE.max
|
||
};
|
||
} else {
|
||
this.thinkingIntervalRange = { ...DEFAULT_INTERVAL_RANGE };
|
||
}
|
||
if (payload && Array.isArray(payload.tool_categories)) {
|
||
this.toolCategories = payload.tool_categories
|
||
.map((item: { id?: string; label?: string } = {}) => ({
|
||
id: typeof item.id === 'string' ? item.id : String(item.id ?? ''),
|
||
label: (item.label && String(item.label)) || (typeof item.id === 'string' ? item.id : String(item.id ?? ''))
|
||
}))
|
||
.filter((item: { id: string }) => !!item.id);
|
||
} else {
|
||
this.toolCategories = [];
|
||
}
|
||
if (payload && Array.isArray(payload.skills_catalog)) {
|
||
this.skillsCatalog = payload.skills_catalog
|
||
.map((item: { id?: string; label?: string; description?: string } = {}) => ({
|
||
id: typeof item.id === 'string' ? item.id : String(item.id ?? ''),
|
||
label: (item.label && String(item.label)) || (typeof item.id === 'string' ? item.id : String(item.id ?? '')),
|
||
description: typeof item.description === 'string' ? item.description : undefined
|
||
}))
|
||
.filter((item: { id: string }) => !!item.id);
|
||
} else {
|
||
this.skillsCatalog = [];
|
||
}
|
||
},
|
||
clearFeedback() {
|
||
this.status = '';
|
||
this.error = '';
|
||
},
|
||
async toggleEnabled() {
|
||
if (this.toggleUpdating) {
|
||
return;
|
||
}
|
||
const newValue = !this.form.enabled;
|
||
const previousValue = this.form.enabled;
|
||
this.toggleUpdating = true;
|
||
this.status = '';
|
||
this.error = '';
|
||
this.form.enabled = newValue;
|
||
try {
|
||
const resp = await fetch('/api/personalization', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ enabled: newValue })
|
||
});
|
||
const result = await resp.json();
|
||
if (!resp.ok || !result.success) {
|
||
throw new Error(result.error || '更新失败');
|
||
}
|
||
if (result.data) {
|
||
this.applyPersonalizationData(result.data);
|
||
}
|
||
this.applyPersonalizationMeta(result);
|
||
const statusLabel = newValue ? '已启用' : '已停用';
|
||
this.status = statusLabel;
|
||
setTimeout(() => {
|
||
if (this.status === statusLabel) {
|
||
this.status = '';
|
||
}
|
||
}, 2000);
|
||
} catch (error: any) {
|
||
this.form.enabled = previousValue;
|
||
this.error = error?.message || '更新失败';
|
||
} finally {
|
||
this.toggleUpdating = false;
|
||
}
|
||
},
|
||
async save() {
|
||
if (this.saving) {
|
||
return;
|
||
}
|
||
this.saving = true;
|
||
this.status = '';
|
||
this.error = '';
|
||
try {
|
||
const resp = await fetch('/api/personalization', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(this.form)
|
||
});
|
||
const result = await resp.json();
|
||
if (!resp.ok || !result.success) {
|
||
throw new Error(result.error || '保存失败');
|
||
}
|
||
this.applyPersonalizationData(result.data || {});
|
||
this.applyPersonalizationMeta(result);
|
||
this.status = '已保存';
|
||
setTimeout(() => {
|
||
if (this.status === '已保存') {
|
||
this.status = '';
|
||
}
|
||
}, 3000);
|
||
} catch (error: any) {
|
||
this.error = error?.message || '保存失败';
|
||
} finally {
|
||
this.saving = false;
|
||
}
|
||
},
|
||
updateField(payload: { key: keyof PersonalForm; value: any }) {
|
||
if (!payload || !payload.key) {
|
||
return;
|
||
}
|
||
this.form = {
|
||
...this.form,
|
||
[payload.key]: payload.value
|
||
};
|
||
this.clearFeedback();
|
||
},
|
||
setThinkingInterval(value: number | null) {
|
||
let target: number | null = value;
|
||
if (typeof target === 'number') {
|
||
if (Number.isNaN(target)) {
|
||
target = null;
|
||
} else {
|
||
const rounded = Math.round(target);
|
||
const min = this.thinkingIntervalRange.min ?? DEFAULT_INTERVAL_RANGE.min;
|
||
const max = this.thinkingIntervalRange.max ?? DEFAULT_INTERVAL_RANGE.max;
|
||
target = Math.max(min, Math.min(max, rounded));
|
||
if (target === this.thinkingIntervalDefault) {
|
||
target = null;
|
||
}
|
||
}
|
||
}
|
||
this.form = {
|
||
...this.form,
|
||
thinking_interval: target
|
||
};
|
||
this.clearFeedback();
|
||
},
|
||
toggleDefaultToolCategory(categoryId: string) {
|
||
if (!categoryId) {
|
||
return;
|
||
}
|
||
const current = new Set(this.form.disabled_tool_categories || []);
|
||
if (current.has(categoryId)) {
|
||
current.delete(categoryId);
|
||
} else {
|
||
current.add(categoryId);
|
||
}
|
||
this.form = {
|
||
...this.form,
|
||
disabled_tool_categories: Array.from(current)
|
||
};
|
||
this.clearFeedback();
|
||
},
|
||
toggleSkill(skillId: string) {
|
||
if (!skillId) {
|
||
return;
|
||
}
|
||
const current = new Set(this.form.enabled_skills || []);
|
||
if (current.has(skillId)) {
|
||
current.delete(skillId);
|
||
} else {
|
||
current.add(skillId);
|
||
}
|
||
this.form = {
|
||
...this.form,
|
||
enabled_skills: Array.from(current)
|
||
};
|
||
this.clearFeedback();
|
||
},
|
||
setDefaultRunMode(mode: RunMode | null) {
|
||
let target: RunMode | null = null;
|
||
if (typeof mode === 'string' && RUN_MODE_OPTIONS.includes(mode as RunMode)) {
|
||
target = mode as RunMode;
|
||
}
|
||
this.form = {
|
||
...this.form,
|
||
default_run_mode: target
|
||
};
|
||
this.clearFeedback();
|
||
},
|
||
setDefaultModel(model: string | null) {
|
||
const allowed = ['deepseek', 'kimi-k2.5', 'kimi', 'qwen3-vl-plus', 'minimax-m2.5'];
|
||
const target = typeof model === 'string' && allowed.includes(model) ? model : null;
|
||
this.form = {
|
||
...this.form,
|
||
default_model: target
|
||
};
|
||
this.clearFeedback();
|
||
},
|
||
setImageCompression(mode: string) {
|
||
const allowed = ['original', '1080p', '720p', '540p'];
|
||
if (!allowed.includes(mode)) return;
|
||
this.form = {
|
||
...this.form,
|
||
image_compression: mode
|
||
};
|
||
this.clearFeedback();
|
||
},
|
||
applyTonePreset(preset: string) {
|
||
if (!preset) {
|
||
return;
|
||
}
|
||
this.form = {
|
||
...this.form,
|
||
tone: preset
|
||
};
|
||
this.clearFeedback();
|
||
},
|
||
updateNewConsideration(value: string) {
|
||
this.newConsideration = value;
|
||
this.clearFeedback();
|
||
},
|
||
addConsideration() {
|
||
if (!this.newConsideration) {
|
||
return;
|
||
}
|
||
if (this.form.considerations.length >= this.maxConsiderations) {
|
||
return;
|
||
}
|
||
this.form = {
|
||
...this.form,
|
||
considerations: [...this.form.considerations, this.newConsideration]
|
||
};
|
||
this.newConsideration = '';
|
||
this.clearFeedback();
|
||
},
|
||
removeConsideration(index: number) {
|
||
const items = [...this.form.considerations];
|
||
items.splice(index, 1);
|
||
this.form = {
|
||
...this.form,
|
||
considerations: items
|
||
};
|
||
this.clearFeedback();
|
||
},
|
||
considerationDragStart(index: number, event: DragEvent) {
|
||
this.draggedConsiderationIndex = index;
|
||
if (event && event.dataTransfer) {
|
||
event.dataTransfer.effectAllowed = 'move';
|
||
}
|
||
},
|
||
considerationDragOver(index: number, event: DragEvent) {
|
||
if (event) {
|
||
event.preventDefault();
|
||
}
|
||
if (this.draggedConsiderationIndex === null || this.draggedConsiderationIndex === index) {
|
||
return;
|
||
}
|
||
const items = [...this.form.considerations];
|
||
const [moved] = items.splice(this.draggedConsiderationIndex, 1);
|
||
items.splice(index, 0, moved);
|
||
this.form = {
|
||
...this.form,
|
||
considerations: items
|
||
};
|
||
this.draggedConsiderationIndex = index;
|
||
this.clearFeedback();
|
||
},
|
||
considerationDrop(index: number, event: DragEvent) {
|
||
if (event) {
|
||
event.preventDefault();
|
||
}
|
||
this.considerationDragEnd();
|
||
this.considerationDragOver(index, event);
|
||
},
|
||
considerationDragEnd() {
|
||
this.draggedConsiderationIndex = null;
|
||
},
|
||
async logout() {
|
||
try {
|
||
const resp = await fetch('/logout', { method: 'POST' });
|
||
let result: any = {};
|
||
try {
|
||
result = await resp.json();
|
||
} catch (err) {
|
||
result = {};
|
||
}
|
||
if (!resp.ok || (result && result.success === false)) {
|
||
const message = (result && (result.error || result.message)) || '退出失败';
|
||
throw new Error(message);
|
||
}
|
||
window.location.href = '/login';
|
||
} catch (error: any) {
|
||
console.error('退出登录失败:', error);
|
||
this.error = error?.message || '退出登录失败,请稍后重试';
|
||
}
|
||
},
|
||
persistExperiments() {
|
||
if (typeof window === 'undefined' || !window.localStorage) {
|
||
return;
|
||
}
|
||
try {
|
||
window.localStorage.setItem(EXPERIMENT_STORAGE_KEY, JSON.stringify(this.experiments));
|
||
} catch (error) {
|
||
console.warn('写入实验功能设置失败:', error);
|
||
}
|
||
},
|
||
setLiquidGlassExperimentEnabled(enabled: boolean) {
|
||
this.experiments = {
|
||
...this.experiments,
|
||
liquidGlassEnabled: !!enabled
|
||
};
|
||
this.persistExperiments();
|
||
},
|
||
toggleLiquidGlassExperiment() {
|
||
this.setLiquidGlassExperimentEnabled(!this.experiments.liquidGlassEnabled);
|
||
},
|
||
setBlockDisplayMode(mode: BlockDisplayMode) {
|
||
this.experiments = {
|
||
...this.experiments,
|
||
blockDisplayMode: mode
|
||
};
|
||
this.persistExperiments();
|
||
},
|
||
updateLiquidGlassPosition(position: LiquidGlassPosition | null) {
|
||
this.experiments = {
|
||
...this.experiments,
|
||
liquidGlassPosition: position
|
||
};
|
||
this.persistExperiments();
|
||
}
|
||
}
|
||
});
|