agent-Specialization/static/src/stores/personalization.ts
JOJO 321818f7ef feat: 新增极简模式(Minimal Mode)
新增了一个全新的消息显示模式,提供更简洁的对话体验。

主要功能:
- 新增 MinimalBlocks.vue 组件,实现极简风格的消息块展示
- 在 ChatArea.vue 中集成极简模式渲染逻辑
- 在个性化设置中添加"极简模式"选项(blockDisplayMode)
- 支持三种显示模式:堆叠模式、极简模式、传统模式

技术实现:
- 使用 personalization store 管理显示模式状态
- 通过 computed 属性动态切换渲染组件
- 保持与现有堆叠模式和传统模式的兼容性

用户体验:
- 更清爽的界面布局
- 减少视觉干扰
- 适合快速浏览对话内容

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-04-04 14:47:45 +08:00

562 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
}
});