agent-Specialization/static/src/stores/personalization.ts
JOJO 2f75c1c8bb feat: stable version before virtual monitor timing fix
Current status includes:
- Virtual monitor surface and components
- Monitor store for state management
- Tool call animations and transitions
- Liquid glass shader integration

Known issue to fix: Tool status display timing - "正在xx" appears
after tool execution completes instead of when tool call starts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-13 17:12:12 +08:00

461 lines
14 KiB
TypeScript

import { defineStore } from 'pinia';
type RunMode = 'fast' | 'thinking' | 'deep';
interface PersonalForm {
enabled: boolean;
self_identify: string;
user_name: string;
profession: string;
tone: string;
considerations: string[];
thinking_interval: number | null;
disabled_tool_categories: string[];
default_run_mode: RunMode | null;
}
interface LiquidGlassPosition {
left: number;
top: number;
}
interface ExperimentState {
liquidGlassEnabled: boolean;
liquidGlassPosition: LiquidGlassPosition | null;
}
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 }>;
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,
self_identify: '',
user_name: '',
profession: '',
tone: '',
considerations: [],
thinking_interval: null,
disabled_tool_categories: [],
default_run_mode: null
});
const defaultExperimentState = (): ExperimentState => ({
liquidGlassEnabled: false,
liquidGlassPosition: null
});
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);
return {
liquidGlassEnabled: Boolean(parsed?.liquidGlassEnabled),
liquidGlassPosition: isValidPosition(parsed?.liquidGlassPosition) ? parsed?.liquidGlassPosition : null
};
} 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: [],
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) {
this.form = {
enabled: !!data.enabled,
self_identify: data.self_identify || '',
user_name: data.user_name || '',
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
};
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 = [];
}
},
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: string }) {
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();
},
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();
},
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);
},
updateLiquidGlassPosition(position: LiquidGlassPosition | null) {
this.experiments = {
...this.experiments,
liquidGlassPosition: position
};
this.persistExperiments();
}
}
});