feat: enhance personal space experience

This commit is contained in:
JOJO 2025-11-22 18:14:23 +08:00
parent 7c2cc93585
commit d0af9755c6
4 changed files with 927 additions and 29 deletions

View File

@ -0,0 +1,174 @@
"""Utilities for managing per-user personalization settings."""
from __future__ import annotations
import json
from copy import deepcopy
from pathlib import Path
from typing import Any, Dict, Iterable, Optional, Union
PERSONALIZATION_FILENAME = "personalization.json"
MAX_SHORT_FIELD_LENGTH = 20
MAX_CONSIDERATION_LENGTH = 50
MAX_CONSIDERATION_ITEMS = 10
TONE_PRESETS = ["健谈", "幽默", "直言不讳", "鼓励性", "诗意", "企业商务", "打破常规", "同理心"]
DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = {
"enabled": False,
"self_identify": "",
"user_name": "",
"profession": "",
"tone": "",
"considerations": [],
}
__all__ = [
"PERSONALIZATION_FILENAME",
"DEFAULT_PERSONALIZATION_CONFIG",
"TONE_PRESETS",
"MAX_CONSIDERATION_ITEMS",
"load_personalization_config",
"save_personalization_config",
"ensure_personalization_config",
"build_personalization_prompt",
"sanitize_personalization_payload",
]
PathLike = Union[str, Path]
def _ensure_parent(path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
def _to_path(base: PathLike) -> Path:
base_path = Path(base).expanduser()
if base_path.is_dir():
return base_path / PERSONALIZATION_FILENAME
return base_path
def ensure_personalization_config(base_dir: PathLike) -> Dict[str, Any]:
"""Ensure the personalization file exists and return its content."""
path = _to_path(base_dir)
_ensure_parent(path)
if not path.exists():
with open(path, "w", encoding="utf-8") as f:
json.dump(DEFAULT_PERSONALIZATION_CONFIG, f, ensure_ascii=False, indent=2)
return deepcopy(DEFAULT_PERSONALIZATION_CONFIG)
return load_personalization_config(base_dir)
def load_personalization_config(base_dir: PathLike) -> Dict[str, Any]:
"""Load personalization config; fall back to defaults on errors."""
path = _to_path(base_dir)
_ensure_parent(path)
if not path.exists():
return ensure_personalization_config(base_dir)
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return sanitize_personalization_payload(data)
except (json.JSONDecodeError, OSError):
# 重置为默认配置,避免错误阻塞
with open(path, "w", encoding="utf-8") as f:
json.dump(DEFAULT_PERSONALIZATION_CONFIG, f, ensure_ascii=False, indent=2)
return deepcopy(DEFAULT_PERSONALIZATION_CONFIG)
def sanitize_personalization_payload(
payload: Optional[Dict[str, Any]],
fallback: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Normalize payload structure and clamp field lengths."""
base = deepcopy(DEFAULT_PERSONALIZATION_CONFIG)
if fallback:
base.update(fallback)
data = payload or {}
def _resolve_short_field(key: str) -> str:
if key in data:
return _sanitize_short_field(data.get(key))
return _sanitize_short_field(base.get(key))
base["enabled"] = bool(data.get("enabled", base["enabled"]))
base["self_identify"] = _resolve_short_field("self_identify")
base["user_name"] = _resolve_short_field("user_name")
base["profession"] = _resolve_short_field("profession")
base["tone"] = _resolve_short_field("tone")
if "considerations" in data:
base["considerations"] = _sanitize_considerations(data.get("considerations"))
else:
base["considerations"] = _sanitize_considerations(base.get("considerations", []))
return base
def save_personalization_config(base_dir: PathLike, payload: Dict[str, Any]) -> Dict[str, Any]:
"""Persist sanitized personalization config and return it."""
existing = load_personalization_config(base_dir)
config = sanitize_personalization_payload(payload, fallback=existing)
path = _to_path(base_dir)
_ensure_parent(path)
with open(path, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=2)
return config
def build_personalization_prompt(
config: Optional[Dict[str, Any]],
include_header: bool = True
) -> Optional[str]:
"""Generate the personalization prompt text based on config."""
if not config or not config.get("enabled"):
return None
lines = []
if include_header:
lines.append("用户的个性化数据,请回答时务必参照这些信息")
if config.get("self_identify"):
lines.append(f"用户希望你自称:{config['self_identify']}")
if config.get("user_name"):
lines.append(f"用户希望你称呼为:{config['user_name']}")
if config.get("profession"):
lines.append(f"用户的职业是:{config['profession']}")
if config.get("tone"):
lines.append(f"用户希望你使用 {config['tone']} 的语气与TA交流")
considerations: Iterable[str] = config.get("considerations") or []
considerations = [item for item in considerations if item]
if considerations:
lines.append("用户希望你在回答问题时必须考虑的信息是:")
for idx, item in enumerate(considerations, 1):
lines.append(f"{idx}. {item}")
if len(lines) == (1 if include_header else 0):
# 没有任何有效内容时不注入
return None
return "\n".join(lines)
def _sanitize_short_field(value: Optional[str]) -> str:
if not value:
return ""
text = str(value).strip()
if not text:
return ""
return text[:MAX_SHORT_FIELD_LENGTH]
def _sanitize_considerations(value: Any) -> list:
if not isinstance(value, list):
return []
cleaned = []
for item in value:
if not isinstance(item, str):
continue
text = item.strip()
if not text:
continue
cleaned.append(text[:MAX_CONSIDERATION_LENGTH])
if len(cleaned) >= MAX_CONSIDERATION_ITEMS:
break
return cleaned

View File

@ -254,6 +254,24 @@ async function bootstrapApp() {
currentConversationId: null,
currentConversationTitle: '当前对话',
personalPageVisible: false,
personalizationLoading: false,
personalizationSaving: false,
personalizationLoaded: false,
personalizationStatus: '',
personalizationError: '',
personalizationMaxConsiderations: 10,
overlayPressActive: false,
personalForm: {
enabled: false,
self_identify: '',
user_name: '',
profession: '',
tone: '',
considerations: []
},
tonePresets: ['健谈', '幽默', '直言不讳', '鼓励性', '诗意', '企业商务', '打破常规', '同理心'],
newConsideration: '',
draggedConsiderationIndex: null,
// 搜索功能
searchQuery: '',
@ -865,11 +883,12 @@ async function bootstrapApp() {
lastAction.content += data.content;
}
this.$forceUpdate();
this.$nextTick(() => {
if (lastAction && lastAction.blockId) {
this.$nextTick(() => this.scrollThinkingToBottom(lastAction.blockId));
} else {
this.conditionalScrollToBottom();
this.scrollThinkingToBottom(lastAction.blockId);
}
this.conditionalScrollToBottom();
});
}
});
@ -1606,6 +1625,9 @@ async function bootstrapApp() {
} else {
this.conversations.push(...data.data.conversations);
}
if (this.currentConversationId) {
this.promoteConversationToTop(this.currentConversationId);
}
this.hasMoreConversations = data.data.has_more;
console.log(`已加载 ${this.conversations.length} 个对话`);
@ -2143,12 +2165,185 @@ async function bootstrapApp() {
this.sidebarCollapsed = !this.sidebarCollapsed;
},
openPersonalPage() {
async openPersonalPage() {
this.personalPageVisible = true;
if (!this.personalizationLoaded && !this.personalizationLoading) {
await this.fetchPersonalization();
}
},
closePersonalPage() {
this.personalPageVisible = false;
this.draggedConsiderationIndex = null;
this.overlayPressActive = false;
},
handleOverlayPressStart(event) {
if (event && event.type === 'mousedown' && event.button !== 0) {
return;
}
this.overlayPressActive = true;
},
handleOverlayPressEnd() {
if (!this.overlayPressActive) {
return;
}
this.overlayPressActive = false;
this.closePersonalPage();
},
handleOverlayPressCancel() {
this.overlayPressActive = false;
},
async handleLogout() {
try {
const resp = await fetch('/logout', { method: 'POST' });
let result = {};
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) {
console.error('退出登录失败:', error);
alert(`退出登录失败:${error.message || '请稍后重试'}`);
}
},
async fetchPersonalization() {
this.personalizationLoading = true;
this.personalizationError = '';
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.personalizationLoaded = true;
} catch (error) {
this.personalizationError = error.message || '加载失败';
alert(`加载个性化配置失败:${this.personalizationError}`);
} finally {
this.personalizationLoading = false;
}
},
applyPersonalizationData(data) {
this.personalForm = {
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] : []
};
this.clearPersonalizationFeedback();
},
clearPersonalizationFeedback() {
this.personalizationStatus = '';
this.personalizationError = '';
},
async savePersonalization() {
if (this.personalizationSaving) {
return;
}
this.personalizationSaving = true;
this.personalizationStatus = '';
this.personalizationError = '';
try {
const resp = await fetch('/api/personalization', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.personalForm)
});
const result = await resp.json();
if (!resp.ok || !result.success) {
throw new Error(result.error || '保存失败');
}
this.applyPersonalizationData(result.data || {});
this.personalizationStatus = '已保存';
setTimeout(() => {
this.personalizationStatus = '';
}, 3000);
} catch (error) {
this.personalizationError = error.message || '保存失败';
alert(`保存个性化配置失败:${this.personalizationError}`);
} finally {
this.personalizationSaving = false;
}
},
addConsideration() {
if (!this.newConsideration) {
return;
}
if (this.personalForm.considerations.length >= this.personalizationMaxConsiderations) {
alert(`最多添加 ${this.personalizationMaxConsiderations} 条信息`);
return;
}
this.personalForm.considerations = [
...this.personalForm.considerations,
this.newConsideration
];
this.newConsideration = '';
this.clearPersonalizationFeedback();
},
removeConsideration(index) {
const items = [...this.personalForm.considerations];
items.splice(index, 1);
this.personalForm.considerations = items;
this.clearPersonalizationFeedback();
},
applyTonePreset(preset) {
this.personalForm.tone = preset;
this.clearPersonalizationFeedback();
},
handleConsiderationDragStart(index, event) {
this.draggedConsiderationIndex = index;
if (event && event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
}
},
handleConsiderationDragOver(index, event) {
if (event) {
event.preventDefault();
}
if (this.draggedConsiderationIndex === null || this.draggedConsiderationIndex === index) {
return;
}
const items = [...this.personalForm.considerations];
const [moved] = items.splice(this.draggedConsiderationIndex, 1);
items.splice(index, 0, moved);
this.personalForm.considerations = items;
this.draggedConsiderationIndex = index;
this.clearPersonalizationFeedback();
},
handleConsiderationDrop(index, event) {
if (event) {
event.preventDefault();
}
this.draggedConsiderationIndex = null;
},
handleConsiderationDragEnd() {
this.draggedConsiderationIndex = null;
},
async toggleThinkingMode() {

View File

@ -812,10 +812,24 @@
<transition name="personal-page-fade">
<div class="personal-page-overlay"
v-if="personalPageVisible"
@click.self="closePersonalPage">
@mousedown.self="handleOverlayPressStart"
@mouseup.self="handleOverlayPressEnd"
@mouseleave.self="handleOverlayPressCancel"
@touchstart.self.prevent="handleOverlayPressStart"
@touchend.self.prevent="handleOverlayPressEnd"
@touchcancel.self="handleOverlayPressCancel">
<div class="personal-page-card">
<div class="personal-page-header">
<div>
<h2>个人空间</h2>
<p>敬请期待,个人页面正在建设中。</p>
<p>配置 AI 智能体的个性化偏好</p>
</div>
<div class="personal-page-actions">
<button type="button"
class="personal-page-logout"
@click="handleLogout">
退出登录
</button>
<button type="button"
class="personal-page-close"
@click="closePersonalPage">
@ -823,6 +837,134 @@
</button>
</div>
</div>
<div class="personalization-body" v-if="!personalizationLoading">
<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"
v-model="personalForm.enabled"
@change="clearPersonalizationFeedback">
<span class="switch-slider"></span>
</span>
</label>
<form class="personal-form" @submit.prevent="savePersonalization">
<div class="personalization-sections">
<div class="personal-section personal-info">
<label class="personal-field">
<span>您希望AI智能体怎么自称</span>
<input type="text"
v-model.trim="personalForm.self_identify"
maxlength="20"
placeholder="如小秘、助理小A"
@input="clearPersonalizationFeedback">
</label>
<label class="personal-field">
<span>您希望AI智能体怎么称呼您</span>
<input type="text"
v-model.trim="personalForm.user_name"
maxlength="20"
placeholder="如Jojo、老师"
@input="clearPersonalizationFeedback">
</label>
<label class="personal-field">
<span>您的职业是?</span>
<input type="text"
v-model.trim="personalForm.profession"
maxlength="20"
placeholder="如:产品经理、设计师"
@input="clearPersonalizationFeedback">
</label>
<div class="personal-field">
<label>
<span>您希望AI智能体用何种语气与您交流</span>
<input type="text"
v-model.trim="personalForm.tone"
maxlength="20"
placeholder="请选择或输入语气"
@input="clearPersonalizationFeedback">
</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="applyTonePreset(preset)">
{{ preset }}
</button>
</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"
v-model.trim="newConsideration"
maxlength="50"
placeholder="输入后点击 + 号添加"
@input="clearPersonalizationFeedback">
<button type="button"
class="consideration-add"
:disabled="!newConsideration || personalForm.considerations.length >= personalizationMaxConsiderations"
@click="addConsideration">
+
</button>
</div>
<ul class="consideration-list" v-if="personalForm.considerations.length">
<li v-for="(item, idx) in personalForm.considerations"
:key="`consideration-${idx}`"
class="consideration-item"
draggable="true"
@dragstart="handleConsiderationDragStart(idx, $event)"
@dragover.prevent="handleConsiderationDragOver(idx, $event)"
@drop.prevent="handleConsiderationDrop(idx, $event)"
@dragend="handleConsiderationDragEnd">
<span class="drag-handle" aria-hidden="true"></span>
<span class="consideration-text">{{ item }}</span>
<button type="button"
class="consideration-remove"
@click="removeConsideration(idx)">
</button>
</li>
</ul>
<p class="consideration-hint" v-else>尚未添加任何必备信息</p>
<p class="consideration-limit">最多 {{ personalizationMaxConsiderations }} 条,可拖动排序</p>
</div>
</div>
<div class="personal-form-actions">
<div class="personal-status-group">
<transition name="personal-status-fade">
<span class="status success"
v-if="personalizationStatus">{{ personalizationStatus }}</span>
</transition>
<transition name="personal-status-fade">
<span class="status error"
v-if="personalizationError">{{ personalizationError }}</span>
</transition>
</div>
<button type="button"
class="primary"
:disabled="personalizationSaving"
@click="savePersonalization">
{{ personalizationSaving ? '保存中...' : '保存设置' }}
</button>
</div>
</div>
</div>
</form>
</div>
<div class="personalization-loading" v-else>
正在加载个性化配置...
</div>
</div>
</div>
</transition>
</template>
<div class="easter-egg-overlay"

View File

@ -2558,40 +2558,56 @@ o-files {
justify-content: center;
z-index: 400;
padding: 20px;
transition: opacity 0.25s ease, backdrop-filter 0.25s ease;
will-change: opacity, backdrop-filter;
}
.personal-page-card {
width: min(90vw, 420px);
width: min(95vw, 860px);
background: #fffaf4;
border-radius: 26px;
border-radius: 24px;
border: 1px solid rgba(118, 103, 84, 0.25);
box-shadow: 0 28px 60px rgba(38, 28, 18, 0.25);
padding: 40px 48px;
text-align: center;
padding: 40px;
text-align: left;
color: var(--claude-text);
max-height: calc(100vh - 40px);
overflow: hidden;
}
.personal-page-card h2 {
font-size: 26px;
margin-bottom: 12px;
.personal-page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 24px;
}
.personal-page-card p {
font-size: 15px;
.personal-page-actions {
display: flex;
gap: 10px;
align-items: flex-start;
}
.personal-page-header h2 {
font-size: 24px;
margin: 0;
}
.personal-page-header p {
margin: 6px 0 0;
color: var(--claude-text-secondary);
margin-bottom: 32px;
font-size: 14px;
}
.personal-page-close {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 22px;
align-self: flex-start;
padding: 8px 18px;
border-radius: 999px;
border: none;
background: linear-gradient(135deg, var(--claude-accent) 0%, var(--claude-accent-strong) 100%);
color: #fffdf8;
font-size: 14px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 14px 28px rgba(189, 93, 58, 0.2);
@ -2603,14 +2619,385 @@ o-files {
box-shadow: 0 18px 34px rgba(189, 93, 58, 0.3);
}
.personal-page-logout {
align-self: flex-start;
padding: 8px 16px;
border-radius: 999px;
border: 1px solid rgba(118, 103, 84, 0.35);
background: rgba(255, 255, 255, 0.92);
color: var(--claude-text);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease, box-shadow 0.2s ease;
}
.personal-page-logout:hover {
background: #fff;
box-shadow: 0 12px 24px rgba(38, 28, 18, 0.12);
}
.personalization-body {
display: flex;
flex-direction: column;
gap: 20px;
overflow-y: auto;
max-height: calc(100vh - 180px);
padding-right: 6px;
scrollbar-width: none;
-ms-overflow-style: none;
}
.personalization-body::-webkit-scrollbar {
display: none;
}
.personal-toggle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 18px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(118, 103, 84, 0.2);
}
.toggle-text {
display: flex;
flex-direction: column;
gap: 6px;
}
.toggle-title {
font-weight: 600;
}
.toggle-desc {
color: var(--claude-text-secondary);
font-size: 13px;
}
.toggle-switch {
position: relative;
width: 46px;
height: 26px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.switch-slider {
position: absolute;
inset: 0;
background-color: #d7d1c5;
border-radius: 30px;
transition: background-color 0.2s ease;
}
.switch-slider::before {
content: "";
position: absolute;
left: 4px;
top: 4px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
transition: transform 0.2s ease;
}
.toggle-switch input:checked + .switch-slider {
background: var(--claude-accent);
}
.toggle-switch input:checked + .switch-slider::before {
transform: translateX(20px);
}
.personal-form {
display: flex;
flex-direction: column;
gap: 18px;
flex: 1;
}
.personalization-sections {
display: flex;
gap: 18px;
align-items: flex-start;
flex-wrap: nowrap;
}
.personal-section {
flex: 1 1 0;
min-width: 240px;
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(118, 103, 84, 0.25);
border-radius: 18px;
padding: 16px 18px;
display: flex;
flex-direction: column;
gap: 16px;
}
.personal-right-column {
display: flex;
flex-direction: column;
gap: 14px;
flex: 1 1 0;
max-width: none;
align-self: stretch;
}
.personal-right-column .personal-section {
flex: 1 1 auto;
}
.personal-section.personal-considerations .personal-field {
flex: 1;
display: flex;
flex-direction: column;
}
.personal-section.personal-considerations .consideration-list {
max-height: 260px;
min-height: 220px;
flex: 1;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.personal-section.personal-considerations .consideration-list::-webkit-scrollbar {
display: none;
}
.personal-section.personal-considerations .consideration-item {
background: rgba(255, 255, 255, 0.95);
}
@media (max-width: 1024px) {
.personalization-sections {
flex-direction: column;
flex-wrap: wrap;
}
.personal-right-column {
width: 100%;
max-width: none;
}
}
.personal-field {
display: flex;
flex-direction: column;
gap: 10px;
font-size: 14px;
}
.personal-field input {
width: 100%;
padding: 10px 14px;
border-radius: 12px;
border: 1px solid rgba(118, 103, 84, 0.4);
background: rgba(255, 255, 255, 0.9);
font-size: 14px;
}
.personal-field input:focus {
outline: 2px solid rgba(189, 93, 58, 0.35);
border-color: rgba(189, 93, 58, 0.6);
}
.tone-preset-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
font-size: 13px;
color: var(--claude-text-secondary);
}
.tone-preset-buttons {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tone-preset-buttons button {
padding: 4px 10px;
border-radius: 999px;
border: 1px solid rgba(118, 103, 84, 0.4);
background: #fff;
font-size: 13px;
cursor: pointer;
}
.tone-preset-buttons button:hover {
border-color: var(--claude-accent);
color: var(--claude-accent);
}
.consideration-input {
display: flex;
gap: 8px;
align-items: center;
}
.consideration-input input {
flex: 1;
}
.consideration-add {
width: 38px;
height: 38px;
border-radius: 12px;
border: none;
background: var(--claude-accent);
color: #fffdf8;
font-size: 20px;
font-weight: 600;
cursor: pointer;
}
.consideration-add:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.consideration-list {
list-style: none;
margin: 6px 0 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.consideration-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 12px;
border: 1px dashed rgba(118, 103, 84, 0.5);
background: rgba(255, 255, 255, 0.9);
cursor: grab;
}
.drag-handle {
font-size: 16px;
color: var(--claude-text-secondary);
}
.consideration-text {
flex: 1;
font-size: 14px;
color: var(--claude-text);
}
.consideration-remove {
border: none;
background: none;
font-size: 18px;
line-height: 1;
cursor: pointer;
color: #d64545;
padding: 0 4px;
}
.consideration-hint,
.consideration-limit {
font-size: 13px;
color: var(--claude-text-secondary);
}
.personal-form-actions {
display: flex;
align-items: center;
gap: 8px;
margin-top: auto;
padding: 12px 0 0;
justify-content: flex-end;
}
.personal-status-group {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 4px;
min-height: 22px;
}
.personal-form-actions .primary {
padding: 10px 20px;
border: none;
border-radius: 999px;
background: linear-gradient(135deg, var(--claude-accent) 0%, var(--claude-accent-strong) 100%);
color: #fff;
font-weight: 600;
cursor: pointer;
}
.personal-form-actions .primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.personal-form-actions .status {
font-size: 13px;
}
.personal-form-actions .status.success {
color: #0f9d58;
}
.personal-form-actions .status.error {
color: #d64545;
}
.personal-status-fade-enter-active,
.personal-status-fade-leave-active {
transition: opacity 0.25s ease;
}
.personal-status-fade-enter-from,
.personal-status-fade-leave-to {
opacity: 0;
}
.personalization-loading {
padding: 32px 0;
text-align: center;
color: var(--claude-text-secondary);
font-size: 14px;
}
}
.personal-page-fade-enter-active,
.personal-page-fade-leave-active {
transition: opacity 0.25s ease;
transition: opacity 0.25s ease, backdrop-filter 0.25s ease;
}
.personal-page-fade-enter-from,
.personal-page-fade-leave-to {
opacity: 0;
backdrop-filter: blur(0);
}
.personal-page-overlay.personal-page-fade-enter-active .personal-page-card,
.personal-page-overlay.personal-page-fade-leave-active .personal-page-card {
transition: transform 0.25s ease, opacity 0.25s ease;
}
.personal-page-overlay.personal-page-fade-enter-from .personal-page-card,
.personal-page-overlay.personal-page-fade-leave-to .personal-page-card {
transform: translateY(18px) scale(0.985);
opacity: 0;
}
/* 彩蛋灌水特效 */