4 Commits

Author SHA1 Message Date
wanghanlin a2971ba2c2 修改样式 4 days ago
wanghanlin b56cea0c8c fix(client): 历史会话时间智能分档显示并修复操作栏按钮 tooltip 4 days ago
wanghanlin 431b1f2b1d fix(client): 修复第三方接入 SDK 后 markdown 排版与标签样式缺失 5 days ago
wanghanlin da031c6ff0 fix(client): 修复重新生成清空历史,优化引用来源/推荐问题与历史滚动 5 days ago
  1. 2
      client/CLAUDE.md
  2. 34
      client/src/api.ts
  3. 68
      client/src/chat.ts
  4. 63
      client/src/dom.ts
  5. 727
      client/src/styles.ts
  6. 21
      client/src/utils.ts
  7. 2
      src/main/resources/static/sdk/test.html

2
client/CLAUDE.md

@ -68,6 +68,8 @@ init(rawConfig)
- **`api.ts`**:HTTP 封装 + SSE 流解析。三种 SSE 接口(普通流式、RAG 流式、同步)的解析逻辑都在这里,注意流式解析的边界处理。
- **`dom.ts`**:纯 DOM 构建与事件绑定,含知识库下拉、RAG 来源卡片、历史会话面板的渲染。
- **`styles.ts`**:所有 CSS 字符串模板,按 `primaryColor` 动态着色,并通过 `--td-*` CSS 变量透传给 TDesign 组件。改动 UI 视觉改这里,**不要**在 `dom.ts` 里写内联样式。
- **主题/聊天变量自包含约定**:TDesign 的变量分为两类,都必须自包含声明在 `.csk-root` 上(`tdThemeVars()` 负责主题变量 `--td-brand/--td-gray/--td-bg/...`,`tdChatVars()` 负责聊天/markdown/input 变量 `--td-chat-*`/`--td-chat-md-*`/`--td-chat-input-*`)。原因:这些变量在 tdesign-web-components 里定义于 `globalCSS` 的 constructable stylesheet、选择器为 `:root`,但 `adoptedStyleSheets` 里的 `:root` 不匹配 shadow root,无法进入 shadow DOM;直接声明在 `.csk-root` 上才能经 CSS 自定义属性继承穿透 shadow DOM。升级 `tdesign-web-components` 版本时需同步核对 `style/index.js``css$2`/`css`/`css$1` 三个变量块。
- **light-DOM 组件样式补注入**:`t-tag` 是 tdesign-web-components 的 light DOM 组件(`isLightDOM: true`),其 `.t-tag` 样式在 `globalCSS` 里只对 shadow DOM 生效、进不了 light DOM,需在 `getStyles()` 里以 `.csk-root .t-tag` 前缀手动补注入(来源 `_chunks/dep-5245073d.js`)。新增 light DOM 组件时同样排查是否需要补样式。
- **`markdown.ts`**:已废弃并移除。原基于 marked 的 Markdown 渲染器改由 TDesign Chat 组件(`t-chat-item`)内置 cherry-markdown 承担,无需额外加载 `marked.min.js`。cherry-markdown 的 XSS 防护由 TDesign 组件保证;如后续需要自定义渲染,仍需维持白名单/转义等安全策略。
### 单例与生命周期

34
client/src/api.ts

@ -652,6 +652,40 @@ export function getConversationExportUrl(conversationId: string): string {
return buildUrl(`/ai/sdk/conversation/${conversationId}/export?${params.toString()}`);
}
/**
* SDK
* userTurn
* DatabaseChatMemory
*/
export async function truncateConversation(conversationId: string, userTurn: number): Promise<boolean> {
if (!currentConfig) return false;
const url = buildUrl(`/ai/sdk/conversation/${conversationId}/truncate`);
try {
const body: Record<string, unknown> = { userTurn };
if (currentConfig.userId) body.accountId = currentConfig.userId;
// 后端按 JSON 数字解析 roleId,须转 Number 传入(空串/非数字不传,避免 0 被误判)
const roleId = getActiveIntegrateId();
if (roleId) {
const num = Number(roleId);
if (Number.isFinite(num) && num > 0) body.roleId = num;
}
const response = await safeFetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!response.ok) {
logger.error(`会话截断失败 status=${response.status}`);
return false;
}
const json: ApiResponse = await response.json();
return json.success === true;
} catch (err) {
logger.error('会话截断异常', err);
return false;
}
}
// ==================== chatId 自动初始化 ====================
/**

68
client/src/chat.ts

@ -16,6 +16,7 @@ import {
fetchConversationMessages,
deleteConversation,
getConversationExportUrl,
truncateConversation,
initChatId,
updateChatId,
getChatId,
@ -64,6 +65,8 @@ let ariaLiveEl: HTMLElement | null = null;
let showLoadingFn: (() => HTMLElement) | null = null;
let hideLoadingFn: (() => void) | null = null;
let isSending = false;
/** 是否正在「重新生成」中(独立于 isSending,覆盖截断后端会话的异步窗口,防止重复触发) */
let isRetrying = false;
/** 当前流式请求的中断控制器(用于"停止生成") */
let abortController: AbortController | null = null;
@ -576,36 +579,55 @@ export function sendQuickReply(text: string): Promise<void> {
/**
* AI ID
* AI AI
* ChatPanel
* AI
*
* "复活"
*/
export async function retryFromMessage(msgId: string): Promise<void> {
if (!config || !messagesContainer || isSending) return;
if (!config || !messagesContainer || isSending || isRetrying) return;
isRetrying = true;
try {
const aiIndex = messages.findIndex(m => m.id === msgId && m.role === 'ai');
if (aiIndex < 0) return;
// 找到上一条用户消息文本
let userText = '';
// 找到该 AI 回复所属的那轮用户提问
let userIndex = -1;
for (let i = aiIndex - 1; i >= 0; i--) {
if (messages[i].role === 'user') { userText = messages[i].content; break; }
if (messages[i].role === 'user') { userIndex = i; break; }
}
if (userIndex < 0) return;
const userText = messages[userIndex].content;
if (!userText) return;
// 移除该 AI 消息及其后所有消息(本地 + DOM)
const removed = messages.splice(aiIndex);
const removedIds = new Set(removed.map(m => m.id));
const nodes = messagesContainer.querySelectorAll('.csk-msg');
nodes.forEach(node => {
const id = (node as HTMLElement).dataset.cskMsgId;
// 无 id 的(如错误气泡、被中断的)按顺序兜底:此处仅按 id 精确移除
if (id && removedIds.has(id)) node.remove();
});
// 计算 userTurn(1-based,仅统计 USER 消息):该轮提问是第几条用户消息
let userTurn = 0;
for (let i = 0; i <= userIndex; i++) {
if (messages[i].role === 'user') userTurn++;
}
// 1. 先截断后端会话(删除该轮提问及其之后的所有消息),失败则不触碰本地数据
const chatId = getChatId();
if (chatId) {
const ok = await truncateConversation(chatId, userTurn);
if (!ok) {
logger.error(`重新生成失败:后端会话截断失败 chatId=${chatId} userTurn=${userTurn}`);
return;
}
}
// 2. 本地截断到该轮提问之前(删除该轮提问、被点击的回复及其后的所有消息)
messages = messages.slice(0, userIndex);
saveMessages(config.integrateId, messages);
updateEmptyState();
// 重新生成(不重复渲染用户气泡)
await produceAIReply(userText);
// 3. 重新渲染历史(清除 DOM 中被删除的消息),保证界面与数据一致
renderHistory();
// 4. 重新发送该轮提问(重新渲染用户气泡 + 生成新回复)
await handleSend(userText);
} finally {
isRetrying = false;
}
}
/** 获取并渲染 RAG 引用来源 */
@ -678,7 +700,15 @@ function renderHistory(): void {
}
isNearBottom = true;
scrollToBottom(messagesContainer);
// 延迟滚动到底部:t-chat-item 的 shadow DOM(含 cherry-markdown)异步渲染,
// 若同步设置 scrollTop,此时 scrollHeight 尚未更新到最终值,历史会话会停在滚动条中间。
// 用双 rAF 等待组件渲染与布局稳定后再滚到底部。
const container = messagesContainer;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (container && container.isConnected) scrollToBottom(container);
});
});
if (clearBtn && messages.length > 0) clearBtn.style.display = 'inline-flex';
updateEmptyState();

63
client/src/dom.ts

@ -8,7 +8,7 @@
* -
*/
import { ResolvedConfig, RagSource } from './types';
import { debounce, formatTime } from './utils';
import { debounce, formatTime, formatHistoryTime } from './utils';
import { t } from './i18n';
import { DialogPlugin } from 'tdesign-web-components/lib/dialog/index.js';
// omi 的 render/h 用于创建带 on* 函数回调的组件(见 createEventful 说明)
@ -915,8 +915,11 @@ function createChatAction(wrapper: HTMLElement, text: string, msgId?: string, fe
const el = actionEl as unknown as {
actionBar: string[];
handleAction: (action: string, data: { event?: Event; active?: boolean }) => void;
tooltipProps: Record<string, unknown>;
};
el.actionBar = ['replay', 'copy', 'good', 'bad'];
// 浮层 portal 到 body:zIndex 越过弹窗(9999)防遮挡,overlayClassName 加 SDK 私有类用于补样式
el.tooltipProps = { overlayClassName: 'csk-tooltip', zIndex: 10000 };
// copyText/comment 为 String 简单类型,须用 setAttribute(property 赋值在 omi 下静默失效)
actionEl.setAttribute('copyText', text);
actionEl.setAttribute('comment', feedback === 'up' ? 'good' : feedback === 'down' ? 'bad' : '');
@ -963,10 +966,11 @@ export function finalizeAIBubble(wrapper: HTMLElement, bubble: HTMLElement): voi
// ==================== P1: RAG 引用来源渲染 ====================
/**
* RAG
* RAG t-button +
* <t-collapse> tdesign-web-components collapse/collapse-panel
* omi provide/inject DOMcreateElement + appendChild
* `this.injection.getCollapseValue is not a function`
* `this.injection.getCollapseValue is not a function`
* t-button DOM
*/
export function renderSources(wrapper: HTMLElement, sources: RagSource[]): void {
// 移除已有的来源卡片
@ -978,24 +982,22 @@ export function renderSources(wrapper: HTMLElement, sources: RagSource[]): void
const sourcesEl = document.createElement('div');
sourcesEl.className = 'csk-sources csk-sources--collapsed';
// 头部
const header = document.createElement('div');
header.className = 'csk-sources__header';
const titleSpan = document.createElement('span');
titleSpan.className = 'csk-sources__title';
titleSpan.textContent = `📚 ${t('source_count', { n: sources.length })}`;
const arrow = document.createElement('span');
arrow.className = 'csk-sources__arrow';
arrow.textContent = '▼';
header.appendChild(titleSpan);
header.appendChild(arrow);
// 折叠开关:t-button(标准组件,紧凑宽度,默认折叠)
const toggle = document.createElement('t-button');
toggle.className = 'csk-sources__toggle';
toggle.setAttribute('theme', 'default');
toggle.setAttribute('variant', 'text');
toggle.setAttribute('size', 'small');
// 点击折叠/展开
header.addEventListener('click', () => {
const updateToggleLabel = () => {
const collapsed = sourcesEl.classList.contains('csk-sources--collapsed');
toggle.textContent = `📚 ${t('source_count', { n: sources.length })} ${collapsed ? '▶' : '▼'}`;
};
updateToggleLabel();
toggle.addEventListener('click', (e) => {
e.stopPropagation();
sourcesEl.classList.toggle('csk-sources--collapsed');
updateToggleLabel();
});
// 内容
@ -1030,7 +1032,7 @@ export function renderSources(wrapper: HTMLElement, sources: RagSource[]): void
body.appendChild(item);
}
sourcesEl.appendChild(header);
sourcesEl.appendChild(toggle);
sourcesEl.appendChild(body);
// 插入到消息气泡(t-chat-item)之后,作为来源卡片紧随气泡下方
@ -1161,8 +1163,9 @@ export function renderHistoryList(
const metaParts: string[] = [];
if (item.roleName) metaParts.push(item.roleName);
if (item.messageCount !== undefined) metaParts.push(`${item.messageCount} 条消息`);
if (item.lastMessageTime) metaParts.push(item.lastMessageTime);
else if (item.createdAt) metaParts.push(item.createdAt);
const timeStr = item.lastMessageTime || item.createdAt;
const timeText = formatHistoryTime(timeStr);
if (timeText) metaParts.push(timeText);
metaEl.textContent = metaParts.join(' · ');
info.appendChild(idEl);
@ -1256,15 +1259,17 @@ export function renderSuggestions(
list.className = 'csk-suggestions__list';
for (const text of suggestions) {
// on* 回调(onClick)必须经 omi render 创建;tag 的 onClick 首参为原生事件,文本经 children 传入
createEventful('t-tag', {
theme: 'primary',
variant: 'light-outline',
onClick: (e: Event) => {
// t-button 走 slot 文本 + addEventListener(内部 fire 派发 CustomEvent),无需 omi render
const btn = document.createElement('t-button');
btn.textContent = text;
btn.setAttribute('theme', 'default');
btn.setAttribute('variant', 'text');
btn.setAttribute('size', 'small');
btn.addEventListener('click', (e) => {
e.stopPropagation();
onClick(text);
},
}, list, text);
});
list.appendChild(btn);
}
section.appendChild(list);

727
client/src/styles.ts

@ -15,6 +15,39 @@ function hexToRgb(hex: string): string {
return `${parseInt(match[1], 16)}, ${parseInt(match[2], 16)}, ${parseInt(match[3], 16)}`;
}
/** 两个 #RRGGBB 颜色之间的 RGB 线性插值;ratio∈[0,1],0=from,1=to */
function mixColor(from: string, to: string, ratio: number): string {
const parse = (hex: string): [number, number, number] | null => {
const m = hex.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);
if (!m) return null;
return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)];
};
const a = parse(from);
const b = parse(to);
if (!a || !b) return from;
const mix = (x: number, y: number) => Math.round(x + (y - x) * ratio);
const r = mix(a[0], b[0]);
const g = mix(a[1], b[1]);
const bl = mix(a[2], b[2]);
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${bl.toString(16).padStart(2, '0')}`;
}
/** TDesign 默认蓝阶梯(primaryColor 非 #RRGGBB 时的回退值,对应 --td-brand-color-1..10) */
const DEFAULT_BRAND_LADDER = ['#f2f3ff', '#d9e1ff', '#b5c7ff', '#8eabff', '#618dff', '#366ef4', '#0052d9', '#003cab', '#002a7c', '#001a57'];
/** 由 primaryColor 生成 10 级品牌色阶梯,index 6(即 --td-brand-color-7)为 primaryColor 本身 */
function brandLadder(hex: string): string[] {
if (!/^#[0-9a-fA-F]{6}$/.test(hex)) return DEFAULT_BRAND_LADDER;
// 浅侧(-1..-6)向白插值,深侧(-8..-10)向黑插值
const lightRatios = [0.96, 0.78, 0.60, 0.42, 0.28, 0.15];
const darkRatios = [0.18, 0.40, 0.62];
const ladder: string[] = [];
for (const r of lightRatios) ladder.push(mixColor(hex, '#ffffff', r));
ladder.push(hex);
for (const r of darkRatios) ladder.push(mixColor(hex, '#000000', r));
return ladder;
}
/** CSS 变量:将配置中的主题色转换为 CSS 自定义属性 */
function cssVars(config: ResolvedConfig): string {
const darker = adjustColor(config.primaryColor, -15);
@ -40,20 +73,6 @@ function cssVars(config: ResolvedConfig): string {
--csk-text-secondary: rgba(0, 0, 0, 0.6);
--csk-text-muted: rgba(0, 0, 0, 0.4);
--csk-bg-app: #FFFFFF;
/* TDesign 组件品牌色覆盖:让 primaryColor 透传到 t-chat-item/t-chat-sender/t-tag 等 */
/* CSS 变量会穿透 shadow DOM,故在 .csk-root 上定义即可被内部组件继承 */
--td-brand-color: ${config.primaryColor};
--td-brand-color-hover: ${darker};
--td-brand-color-active: ${darker};
--td-brand-color-focus: ${config.primaryColor};
--td-brand-color-disabled: ${lighter};
--td-brand-color-light: ${lighter};
--td-brand-color-light-hover: ${lighter};
/* 覆盖 TDesign chat-item 默认 800px 限制,使 AI 回复内容自适应 SDK 弹窗宽度 */
--td-chat-item-content-max-width: 100%;
/* 压缩输入组件:缩小内容区上下 padding 与字号(变量穿透 shadow DOM 生效于 t-chat-sender) */
--td-chat-input-padding: 8px 12px;
--td-chat-input-font-size: 14px;
`;
}
@ -70,11 +89,524 @@ function adjustColor(hex: string, amount: number): string {
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
}
/**
* TDesign light .csk-root 使 SDK shadow DOM
* t-chat-item / t-button / t-tag / t-select / t-chat-action / t-chat-sender
* 宿主页面的 :root --td-*
*
* CSS /
* light .csk-root 上即可免疫宿主对 :root
*
* tdesign-web-components v1.2.10 style/index.js css$2 light
* :root, :root[theme-mode="light"] + :root radius/font/size/comp
* primaryColor tdesign-web-components
*/
function tdThemeVars(config: ResolvedConfig): string {
const darker = adjustColor(config.primaryColor, -15);
const lighter = adjustColor(config.primaryColor, 18);
const [b1, b2, b3, b4, b5, b6, b7, b8, b9, b10] = brandLadder(config.primaryColor);
return `
/* ===== 品牌色阶梯(跟随 primaryColor) ===== */
--td-brand-color-1: ${b1};
--td-brand-color-2: ${b2};
--td-brand-color-3: ${b3};
--td-brand-color-4: ${b4};
--td-brand-color-5: ${b5};
--td-brand-color-6: ${b6};
--td-brand-color-7: ${b7};
--td-brand-color-8: ${b8};
--td-brand-color-9: ${b9};
--td-brand-color-10: ${b10};
/* ===== 警告 / 错误 / 成功阶梯(不跟随主题色) ===== */
--td-warning-color-1: #fff1e9;
--td-warning-color-2: #ffd9c2;
--td-warning-color-3: #ffb98c;
--td-warning-color-4: #fa9550;
--td-warning-color-5: #e37318;
--td-warning-color-6: #be5a00;
--td-warning-color-7: #954500;
--td-warning-color-8: #713300;
--td-warning-color-9: #532300;
--td-warning-color-10: #3b1700;
--td-error-color-1: #fff0ed;
--td-error-color-2: #ffd8d2;
--td-error-color-3: #ffb9b0;
--td-error-color-4: #ff9285;
--td-error-color-5: #f6685d;
--td-error-color-6: #d54941;
--td-error-color-7: #ad352f;
--td-error-color-8: #881f1c;
--td-error-color-9: #68070a;
--td-error-color-10: #490002;
--td-success-color-1: #e3f9e9;
--td-success-color-2: #c6f3d7;
--td-success-color-3: #92dab2;
--td-success-color-4: #56c08d;
--td-success-color-5: #2ba471;
--td-success-color-6: #008858;
--td-success-color-7: #006c45;
--td-success-color-8: #005334;
--td-success-color-9: #003b23;
--td-success-color-10: #002515;
/* ===== 灰阶 ===== */
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c6c6c6;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #393939;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
/* ===== 白 / 灰字体色 ===== */
--td-font-white-1: #ffffff;
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
/* ===== 品牌语义色(沿用 SDK 现有映射,hover/active 加深、light 变浅) ===== */
--td-brand-color: ${config.primaryColor};
--td-brand-color-hover: ${darker};
--td-brand-color-active: ${darker};
--td-brand-color-focus: ${config.primaryColor};
--td-brand-color-disabled: ${lighter};
--td-brand-color-light: ${lighter};
--td-brand-color-light-hover: ${lighter};
/* ===== 警告 / 错误 / 成功 语义色 ===== */
--td-warning-color: var(--td-warning-color-5);
--td-error-color: var(--td-error-color-6);
--td-success-color: var(--td-success-color-5);
--td-warning-color-hover: var(--td-warning-color-4);
--td-warning-color-focus: var(--td-warning-color-2);
--td-warning-color-active: var(--td-warning-color-6);
--td-warning-color-disabled: var(--td-warning-color-3);
--td-warning-color-light: var(--td-warning-color-1);
--td-warning-color-light-hover: var(--td-warning-color-2);
--td-error-color-hover: var(--td-error-color-5);
--td-error-color-focus: var(--td-error-color-2);
--td-error-color-active: var(--td-error-color-7);
--td-error-color-disabled: var(--td-error-color-3);
--td-error-color-light: var(--td-error-color-1);
--td-error-color-light-hover: var(--td-error-color-2);
--td-success-color-hover: var(--td-success-color-4);
--td-success-color-focus: var(--td-success-color-2);
--td-success-color-active: var(--td-success-color-6);
--td-success-color-disabled: var(--td-success-color-3);
--td-success-color-light: var(--td-success-color-1);
--td-success-color-light-hover: var(--td-success-color-2);
/* ===== 遮罩 / 背景 / 文字 / 边框 / 阴影 / 滚动条 ===== */
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
--td-bg-color-page: var(--td-gray-color-2);
--td-bg-color-container: #fff;
--td-bg-color-container-hover: var(--td-gray-color-1);
--td-bg-color-container-active: var(--td-gray-color-3);
--td-bg-color-container-select: #fff;
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
--td-bg-color-component: var(--td-gray-color-3);
--td-bg-color-component-hover: var(--td-gray-color-4);
--td-bg-color-component-active: var(--td-gray-color-6);
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
--td-bg-color-component-disabled: var(--td-gray-color-2);
--td-bg-color-specialcomponent: #fff;
--td-text-color-primary: var(--td-font-gray-1);
--td-text-color-secondary: var(--td-font-gray-2);
--td-text-color-placeholder: var(--td-font-gray-3);
--td-text-color-disabled: var(--td-font-gray-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color-7);
--td-text-color-link: var(--td-brand-color-8);
--td-border-level-1-color: var(--td-gray-color-3);
--td-component-stroke: var(--td-gray-color-3);
--td-border-level-2-color: var(--td-gray-color-4);
--td-component-border: var(--td-gray-color-4);
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 0.05), 0 4px 5px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.12);
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 0.05), 0 8px 10px 1px rgba(0, 0, 0, 0.06), 0 5px 5px -3px rgba(0, 0, 0, 0.1);
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 0.05), 0 16px 24px 2px rgba(0, 0, 0, 0.04), 0 8px 10px -5px rgba(0, 0, 0, 0.08);
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-table-shadow-color: rgba(0, 0, 0, 0.08);
--td-scrollbar-color: rgba(0, 0, 0, 0.1);
--td-scrollbar-hover-color: rgba(0, 0, 0, 0.3);
--td-scroll-track-color: #fff;
/* ===== 圆角 ===== */
--td-radius-small: 2px;
--td-radius-default: 3px;
--td-radius-medium: 6px;
--td-radius-large: 9px;
--td-radius-extraLarge: 12px;
--td-radius-round: 999px;
--td-radius-circle: 50%;
/* ===== 字体 ===== */
--td-font-family: PingFang SC, Microsoft YaHei, Arial Regular;
--td-font-family-medium: PingFang SC, Microsoft YaHei, Arial Medium;
--td-font-size-link-small: 12px;
--td-font-size-link-medium: 14px;
--td-font-size-link-large: 16px;
--td-font-size-mark-small: 12px;
--td-font-size-mark-medium: 14px;
--td-font-size-body-small: 12px;
--td-font-size-body-medium: 14px;
--td-font-size-body-large: 16px;
--td-font-size-title-small: 14px;
--td-font-size-title-medium: 16px;
--td-font-size-title-large: 20px;
--td-font-size-headline-small: 24px;
--td-font-size-headline-medium: 28px;
--td-font-size-headline-large: 36px;
--td-font-size-display-medium: 48px;
--td-font-size-display-large: 64px;
--td-line-height-link-small: 20px;
--td-line-height-link-medium: 22px;
--td-line-height-link-large: 24px;
--td-line-height-mark-small: 20px;
--td-line-height-mark-medium: 22px;
--td-line-height-body-small: 20px;
--td-line-height-body-medium: 22px;
--td-line-height-body-large: 24px;
--td-line-height-title-small: 22px;
--td-line-height-title-medium: 24px;
--td-line-height-title-large: 28px;
--td-line-height-headline-small: 32px;
--td-line-height-headline-medium: 36px;
--td-line-height-headline-large: 44px;
--td-line-height-display-medium: 56px;
--td-line-height-display-large: 72px;
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small) var(--td-font-family);
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium) var(--td-font-family);
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large) var(--td-font-family);
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small) var(--td-font-family);
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium) var(--td-font-family);
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small) var(--td-font-family);
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium) var(--td-font-family);
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large) var(--td-font-family);
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small) var(--td-font-family);
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium) var(--td-font-family);
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large) var(--td-font-family);
--td-font-headline-small: 600 var(--td-font-size-headline-small) / var(--td-line-height-headline-small) var(--td-font-family);
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) / var(--td-line-height-headline-medium) var(--td-font-family);
--td-font-headline-large: 600 var(--td-font-size-headline-large) / var(--td-line-height-headline-large) var(--td-font-family);
--td-font-display-medium: 600 var(--td-font-size-display-medium) / var(--td-line-height-display-medium) var(--td-font-family);
--td-font-display-large: 600 var(--td-font-size-display-large) / var(--td-line-height-display-large) var(--td-font-family);
/* ===== 尺寸与间距 ===== */
--td-size-1: 2px;
--td-size-2: 4px;
--td-size-3: 6px;
--td-size-4: 8px;
--td-size-5: 12px;
--td-size-6: 16px;
--td-size-7: 20px;
--td-size-8: 24px;
--td-size-9: 28px;
--td-size-10: 32px;
--td-size-11: 36px;
--td-size-12: 40px;
--td-size-13: 48px;
--td-size-14: 56px;
--td-size-15: 64px;
--td-size-16: 72px;
--td-comp-size-xxxs: var(--td-size-6);
--td-comp-size-xxs: var(--td-size-7);
--td-comp-size-xs: var(--td-size-8);
--td-comp-size-s: var(--td-size-9);
--td-comp-size-m: var(--td-size-10);
--td-comp-size-l: var(--td-size-11);
--td-comp-size-xl: var(--td-size-12);
--td-comp-size-xxl: var(--td-size-13);
--td-comp-size-xxxl: var(--td-size-14);
--td-comp-size-xxxxl: var(--td-size-15);
--td-comp-size-xxxxxl: var(--td-size-16);
--td-pop-padding-s: var(--td-size-2);
--td-pop-padding-m: var(--td-size-3);
--td-pop-padding-l: var(--td-size-4);
--td-pop-padding-xl: var(--td-size-5);
--td-pop-padding-xxl: var(--td-size-6);
--td-comp-paddingLR-xxs: var(--td-size-1);
--td-comp-paddingLR-xs: var(--td-size-2);
--td-comp-paddingLR-s: var(--td-size-4);
--td-comp-paddingLR-m: var(--td-size-5);
--td-comp-paddingLR-l: var(--td-size-6);
--td-comp-paddingLR-xl: var(--td-size-8);
--td-comp-paddingLR-xxl: var(--td-size-10);
--td-comp-paddingTB-xxs: var(--td-size-1);
--td-comp-paddingTB-xs: var(--td-size-2);
--td-comp-paddingTB-s: var(--td-size-4);
--td-comp-paddingTB-m: var(--td-size-5);
--td-comp-paddingTB-l: var(--td-size-6);
--td-comp-paddingTB-xl: var(--td-size-8);
--td-comp-paddingTB-xxl: var(--td-size-10);
--td-comp-margin-xxs: var(--td-size-1);
--td-comp-margin-xs: var(--td-size-2);
--td-comp-margin-s: var(--td-size-4);
--td-comp-margin-m: var(--td-size-5);
--td-comp-margin-l: var(--td-size-6);
--td-comp-margin-xl: var(--td-size-7);
--td-comp-margin-xxl: var(--td-size-8);
--td-comp-margin-xxxl: var(--td-size-10);
--td-comp-margin-xxxxl: var(--td-size-12);
`;
}
/**
* TDesign Chat --td-chat-* / --td-chat-md-* / --td-chat-input-*
* .csk-root
*
* tdesign-web-components globalCSS constructable
* stylesheet选择器为 :root adoptedStyleSheets 里的 :root shadow root
* shadow DOM t-chat-item / t-chat-md-content
* markdown ///退"裸奔"
* .csk-root CSS 穿 shadow DOM
*
* tdesign-web-components v1.2.10 style/index.js csschat-item + markdown
* css$1input两个 :root
*/
function tdChatVars(): string {
return `
/* ===== chat-input ===== */
--td-chat-input-attachments-margin: 0 0 var(--td-comp-margin-xs) 0;
--td-chat-input-textarea-max-height: 134px;
--td-chat-input-textarea-scrollbar-width: 4px;
--td-chat-input-textarea-scrollbar-thumb-radius: 2px;
--td-chat-input-padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-m);
--td-chat-input-radius: 16px;
--td-chat-input-shadow: 0px 0px 0px 2px var(--td-brand-color-focus);
--td-chat-input-background: var(--td-bg-color-specialcomponent);
--td-chat-input-hover-border: var(--td-brand-color);
--td-chat-input-actions-margin: 0;
--td-chat-input-actions-gap: var(--td-comp-margin-m);
--td-chat-input-actions-item-wh: 28px;
--td-chat-input-actions-item-radius: 14px;
--td-chat-input-actions-item-font-size: 20px;
--td-chat-input-box-shadow: none;
--td-chat-input-font-size: 16px;
--td-chat-input-content-default-border: var(--td-component-border);
--td-chat-input-textarea-scrollbar-color: var(--td-scrollbar-color);
--td-chat-input-textarea-scrollbar-hover-color: var(--td-scrollbar-hover-color);
--td-chat-input-button-default-bg: var(--td-bg-color-secondarycomponent);
--td-chat-input-button-default-color: var(--td-bg-color-container);
--td-chat-input-actions-item-color: var(--td-text-color-secondary);
--td-chat-input-actions-item-bg: transparent;
--td-chat-input-actions-item-hover-bg: var(--td-bg-color-container-hover);
/* ===== chat-list / chat-item ===== */
--td-chat-list-padding-bottom: 0;
--td-chat-list-scroll-button-top: calc(100% - 64px);
--td-chat-list-scroll-button-left: 50%;
--td-chat-list-scroll-button-w: 40px;
--td-chat-list-scroll-button-h: 40px;
--td-chat-list-scroll-button-border: 0.5px solid #dcdcdc;
--td-chat-list-scroll-button-radius: 20px;
--td-chat-list-scroll-button-index: 50;
--td-chat-list-scroll-button-transform: translate(-50%, 0);
--td-chat-item-gap: 38px;
--td-chat-item-header-padding: 0;
--td-chat-item-text-header-padding: 0;
--td-chat-item-header-padding-right: 0;
--td-chat-item-name-padding: 0 var(--td-comp-paddingLR-s) 0 0;
--td-chat-item-name-padding-right: 0 0 0 var(--td-comp-paddingLR-l);
--td-chat-item-name-font: var(--td-font-body-medium);
--td-chat-item-content-gap: var(--td-comp-margin-s);
--td-chat-item-content-max-width: 800px;
--td-chat-item-content-base-margin: 0 0 var(--td-comp-margin-xs) 0;
--td-chat-item-content-base-padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
--td-chat-item-content-base-border-color: transparent;
--td-chat-item-content-padding: var(--td-comp-paddingTB-m) 0;
--td-chat-item-text-error-color: var(--td-error-color);
--td-chat-item-user-text-color: var(--td-text-color-secondary);
--td-chat-item-text-max-width: 100%;
--td-chat-item-text-padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
--td-chat-item-status-margin: var(--td-comp-margin-s) 0 0;
--td-chat-item-text-content-padding: 0;
--td-chat-item-text-detail-padding: 0;
--td-chat-item-text-detail-margin: 0;
--td-chat-item-text-detail-text-margin: 0;
--td-chat-item-text-radius: var(--td-radius-extraLarge);
--td-chat-item-text-border: 1px solid transparent;
--td-chat-item-content-border: 1px solid transparent;
--td-chat-item-content-radius: var(--td-radius-extraLarge);
--td-chat-item-system-margin: 0;
--td-chat-item-system-padding: var(--td-comp-paddingTB-xs) var(--td-comp-paddingLR-s);
--td-chat-item-system-radius: var(--td-radius-medium);
--td-chat-item-system-bg-color: var(--td-bg-color-secondarycontainer);
--td-chat-item-system-color: #afafaf;
--td-chat-item-left-avatar-margin: 0 var(--td-comp-margin-xl) 0 var(--td-comp-margin-xs);
--td-chat-item-right-avatar-margin: 0 var(--td-comp-margin-xs) 0 var(--td-comp-margin-xl);
--td-chat-item-avatar-padding: 6px 0 0 0;
--td-chat-item-avatar-has-header-padding: 30px 0 0 0;
--td-chat-item-image-margin: var(--td-comp-margin-m) 0;
--td-chat-item-think-gap-p: 0 0 var(--td-comp-margin-s) 0;
--td-chat-item-think-padding-tb: var(--td-comp-paddingTB-s);
--td-chat-item-think-padding-lr: var(--td-comp-paddingLR-m);
--td-chat-item-think-border: none;
--td-chat-item-think-radius: var(--td-radius-medium);
--td-chat-item-think-title-gap: var(--td-comp-margin-s);
--td-chat-item-think-status-wh: 20px;
--td-chat-item-think-pending-border-width: 2px;
--td-chat-item-think-header-border: none;
--td-chat-item-think-body-bg: none;
--td-chat-item-think-body-border: none;
--td-chat-item-think-content-margin-top: 2px;
--td-chat-item-think-shadow-top: 0;
--td-chat-item-think-shadow-bottom: 0;
--td-chat-item-think-shadow-margin-top: 0;
--td-chat-item-think-shadow-margin-bottom: 0;
--td-chat-item-think-shadow-top-height: 24px;
--td-chat-item-think-shadow-bottom-height: 24px;
--td-chat-item-think-inner-padding: 0 var(--td-comp-paddingLR-m) var(--td-comp-paddingTB-s) var(--td-comp-paddingLR-m);
--td-chat-item-think-inner-border: solid 2px var(--td-component-stroke);
--td-chat-item-think-inner-border-margin-left: 0px;
--td-chat-item-actions-list-margin: var(--td-comp-margin-xs) 0;
--td-chat-item-text-actions-list-margin: var(--td-comp-margin-xs) 0 0 var(--td-comp-margin-l);
--td-chat-item-actions-list-gap: var(--td-comp-margin-xs);
--td-chat-item-actions-list-padding: var(--td-comp-paddingTB-xs) var(--td-comp-paddingLR-xs);
--td-chat-item-actions-list-radius: var(--td-radius-medium);
--td-chat-item-actions-list-border: 1px solid var(--td-component-border);
--td-chat-item-actions-item-padding: var(--td-comp-paddingTB-xs) var(--td-comp-paddingLR-xs);
--td-chat-item-actions-item-radius: 4px;
--td-chat-item-attachments-margin-top: 12px;
--td-chat-item-image-max-width: 150px;
--td-attachment-image-width: 150px;
--td-chat-item-search-height: 36px;
--td-chat-item-search-padding: var(--td-comp-paddingTB-s) var(--td-comp-paddingLR-s);
--td-chat-item-search-radius: 20px;
--td-chat-item-search-icons-margin-right: 3px;
--td-chat-item-search-icon-width: 16px;
--td-chat-item-search-icon-radius: var(--td-radius-circle);
--td-chat-item-search-icon-margin-left: -3px;
--td-chat-item-search-icon-before-left: -1px;
--td-chat-item-search-icon-before-width: 1px;
--td-chat-item-search-icon-before-height: 100%;
--td-chat-item-search-icon-before-radius: 1px;
--td-chat-item-search-link-color: var(--td-brand-color);
--td-chat-item-search-link-hov-color: var(--td-brand-color-hover);
--td-chat-item-suggestion-gap: var(--td-comp-margin-s);
--td-chat-item-suggestion-height: 38px;
--td-chat-item-suggestion-padding: var(--td-comp-paddingTB-s) var(--td-comp-paddingLR-m);
--td-chat-item-suggestion-radius: var(--td-radius-medium);
--td-chat-item-suggestion-border: solid 1px var(--td-component-border);
--td-chat-item-suggestion-arrow-margin: var(--td-comp-margin-s);
--td-chat-font-size: 16px;
--td-chat-item-system-font-size: 12px;
--td-chat-item-suggestion-font-size: 14px;
--td-chat-item-search-link-line-height: 24px;
--td-chat-list-scroll-button-font-size: 24px;
--td-chat-search-font-size: 12px;
--td-chat-item-content-text-align: left;
--td-chat-item-right-content-text-align: right;
--td-chat-item-text-text-align: left;
--td-chat-list-scroll-button-bg: var(--td-bg-color-container);
--td-chat-list-scroll-button-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.08), 0px 16px 24px 2px rgba(0, 0, 0, 0.04), 0px 6px 30px 5px rgba(0, 0, 0, 0.05);
--td-chat-item-error-color: var(--td-error-color);
--td-chat-item-primary-bg: var(--td-bg-color-secondarycontainer);
--td-chat-item-default-bg: var(--td-bg-color-container);
--td-chat-item-outline-border-color: var(--td-component-border);
--td-chat-item-think-bg: var(--td-bg-color-secondarycontainer);
--td-chat-item-think-pending-border-bg-color: #e0f0ff;
--td-chat-item-think-status-pending-color: var(--td-brand-color);
--td-chat-item-think-status-sent-color: var(--td-success-color);
--td-chat-item-think-status-error-color: var(--td-error-color);
--td-chat-item-think-content-color: var(--td-text-color-placeholder);
--td-chat-item-actions-list-bg: var(--td-bg-color-secondarycontainer);
--td-chat-item-actions-item-hover-bg: var(--td-bg-color-component-hover);
--td-chat-item-suggestion-color: var(--td-text-color-primary);
--td-chat-item-suggestion-background: var(--td-bg-color-secondarycontainer);
--td-chat-item-suggestion-arrow-color: var(--td-text-color-placeholder);
/* ===== chat-md(markdown 排版) ===== */
--md-heading-color: var(--td-gray-color-7);
--td-chat-md-content-gap-main: 0 0 8px 0;
--td-chat-md-content-gap-t1: 24px 0 20px 0;
--td-chat-md-content-gap-t2: 20px 0 16px 0;
--td-chat-md-content-gap-t3: 16px 0 12px 0;
--td-chat-md-font: var(--td-text-color-primary);
--td-chat-md-padding: var(--td-comp-paddingTB-m) 0;
--td-chat-md-h1-font: 700 22px / 1.6 var(--td-font-family);
--td-chat-md-h2-font: 600 20px / 1.6 var(--td-font-family);
--td-chat-md-h3-font: 600 18px / 1.6 var(--td-font-family);
--td-chat-md-s-color: var(--td-gray-color-7);
--td-chat-md-list-margin: 0 0 8px 2em;
--td-chat-md-list-padding: 0;
--td-chat-md-ul-style: disc;
--td-chat-md-ol-style: decimal;
--td-chat-md-li-margin: 0.5em 0;
--td-chat-md-link-color: var(--td-brand-color-5);
--td-chat-md-link-decoration: none;
--td-chat-md-link-hover-color: var(--td-brand-color-6);
--td-chat-md-code-radius: 8px;
--td-chat-md-code-margin: 0 0 16px 0;
--td-chat-md-code-header-height: 36px;
--td-chat-md-code-header-padding: 0 16px 0 12px;
--td-chat-md-code-header-font: 500 14px / 1.6 var(--td-font-family);
--td-chat-md-code-header-bg: #f3f3f3;
--td-chat-md-code-header-bg-dark: #282c34;
--td-chat-md-code-header-color-dark: #abb2bf;
--td-chat-md-code-header-copy-color: var(--td-gray-color-7);
--td-chat-md-code-header-copy-hover-color: var(--td-gray-color-9);
--td-chat-md-code-body-padding: 10px 16px !important;
--td-chat-md-code-body-bg: #f8f8f8;
--td-chat-md-code-body-overflow: auto;
--td-chat-md-code-body-code-font-size: 14px;
--td-chat-md-katex-block-margin: 1em 0;
--td-chat-md-katex-font: normal 1.21em / 1.2 KaTeX_Main, Times New Roman, serif;
--td-chat-md-blockquote-margin: 16px 0;
--td-chat-md-blockquote-padding: 10px 15px;
--td-chat-md-blockquote-border-left: 4px solid #d9d9d9;
--td-chat-md-blockquote-color: var(--td-gray-color-7);
--td-chat-md-footnote-bg: var(--td-bg-color-page);
--td-chat-md-hr-margin: 16px 0;
--td-chat-md-hr-border: 1px solid rgba(0, 0, 0, 0.08);
--td-chat-md-table-border: 1px solid var(--td-bg-color-secondarycontainer);
--td-chat-md-table-radius: 8px;
--td-chat-md-table-margin: 8px 0;
--td-chat-md-table-font-size: 14px;
--td-chat-md-table-th-padding: 0.66em 1em;
--td-chat-md-table-th-max-width: 448px;
--td-chat-md-table-th-bg: var(--td-bg-color-secondarycontainer);
--td-chat-md-table-th-color: var(--td-text-color-primary);
--td-chat-md-table-th-font: 700 14px / 1.6 var(--td-font-family);
--td-chat-md-table-td-bg-odd: var(--td-bg-color-container);
--td-chat-md-table-td-bg-even: var(--td-bg-color-secondarycontainer);
--td-chat-md-table-td-color: var(--td-text-color-primary);
--td-chat-md-table-td-font: 400 14px / 1.6 var(--td-font-family);
/* ===== SDK 尺寸覆盖:AI 回复自适应弹窗宽度 + 压缩输入组件 ===== */
--td-chat-item-content-max-width: 100%;
--td-chat-input-padding: 8px 12px;
--td-chat-input-font-size: 14px;
`;
}
/** 完整 CSS 样式表 */
function getStyles(config: ResolvedConfig): string {
return `
/* ChatbotSDK 样式 - csk- 命名空间 */
.csk-root {
${tdThemeVars(config)}
${tdChatVars()}
${cssVars(config)}
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 14px;
@ -86,6 +618,132 @@ function getStyles(config: ResolvedConfig): string {
}
.csk-root *, .csk-root *::before, .csk-root *::after { box-sizing: border-box; }
/* 原生表单控件字体继承(UA 默认不继承 font,宿主无 reset 时用系统字体) */
.csk-root button, .csk-root input, .csk-root textarea, .csk-root select { font-family: inherit; }
/* launcher/teaser 挂在 body、在 .csk-root 之外,补 box-sizing 防御宿主全局 reset */
.csk-launcher, .csk-teaser { box-sizing: border-box; }
.csk-launcher *, .csk-teaser * { box-sizing: border-box; }
/* ===== tooltip t-popup portal bodylight DOM =====
* .t-tooltip globalCSSconstructable stylesheet shadow DOM
* portal document.body light DOM csk-tooltip 宿
* tdesign-web-components v1.2.10 _chunks/dep-a3fbe4be.jscss_248z */
.csk-tooltip .t-popup__content {
display: inline-block;
border: 0;
z-index: 5600;
margin-bottom: 1px;
max-width: 480px;
word-break: break-word;
box-sizing: border-box;
border-radius: var(--td-radius-medium);
color: var(--td-text-color-anti);
background: var(--td-gray-color-13);
box-shadow: inset 0 0.5px 0 var(--td-gray-color-9), inset 0.5px 0 0 var(--td-gray-color-9), inset 0 -0.5px 0 var(--td-gray-color-9), inset -0.5px 0 0 var(--td-gray-color-9);
}
.csk-tooltip .t-popup__arrow { background: inherit; height: auto; }
.csk-tooltip .t-popup__arrow::before { background: inherit; }
/* ===== t-taglight DOM =====
* t-tag tdesign-web-components light DOM isLightDOM: true .t-tag
* globalCSS constructable stylesheet globalCSS shadow DOM
* light DOM/"裸奔" .t-tag light DOM
* tdesign-web-components v1.2.10 _chunks/dep-5245073d.jscss_248z */
.csk-root .t-tag {
font: var(--td-font-body-medium);
color: var(--td-text-color-primary);
box-sizing: border-box;
margin: 0;
padding: 0;
list-style: none;
display: inline-flex;
align-items: center;
flex-direction: row;
padding: 0px var(--td-comp-paddingLR-s);
height: var(--td-comp-size-xs);
font: var(--td-font-body-small);
color: var(--td-text-color-anti);
border-radius: var(--td-radius-default);
border: 1px solid transparent;
white-space: nowrap;
}
.csk-root .t-tag .t-icon {
margin-right: var(--td-comp-margin-xs);
width: calc(var(--td-font-size-body-medium) + 2px);
height: calc(var(--td-font-size-body-medium) + 2px);
flex-shrink: 0;
}
.csk-root .t-tag .t-tag__icon-close {
margin-right: 0;
margin-left: var(--td-comp-margin-s);
font: var(--td-font-body-medium);
cursor: pointer;
transition: color 0.2s cubic-bezier(0.38, 0, 0.24, 1);
}
.csk-root .t-tag--default { color: var(--td-text-color-primary); background-color: var(--td-bg-color-component); }
.csk-root .t-tag--default .t-tag__icon-close { color: var(--td-text-color-placeholder); }
.csk-root .t-tag--default .t-tag__icon-close:hover { color: var(--td-text-color-primary); }
.csk-root .t-tag--default:active { cursor: default; }
.csk-root .t-tag--default.t-tag--disabled {
color: var(--td-text-color-disabled);
background-color: var(--td-bg-color-component-disabled);
border-color: var(--td-component-border);
cursor: not-allowed;
}
.csk-root .t-tag--default.t-tag--disabled:hover { cursor: not-allowed; }
.csk-root .t-tag--default.t-tag--disabled .t-icon:hover { cursor: pointer; }
.csk-root .t-tag--default.t-tag--outline { border-color: var(--td-component-border); }
.csk-root .t-tag--default.t-tag--light { background-color: var(--td-bg-color-secondarycontainer); }
.csk-root .t-tag--default.t-tag--light-outline { border-color: var(--td-component-border); background-color: var(--td-bg-color-secondarycontainer); }
.csk-root .t-tag--primary { background-color: var(--td-brand-color); }
.csk-root .t-tag--primary.t-tag--outline { border-color: var(--td-brand-color); }
.csk-root .t-tag--primary.t-tag--light { background-color: var(--td-brand-color-light); }
.csk-root .t-tag--primary.t-tag--light-outline { border-color: var(--td-brand-color); background-color: var(--td-brand-color-light); }
.csk-root .t-tag--primary.t-tag--light,
.csk-root .t-tag--primary.t-tag--outline,
.csk-root .t-tag--primary.t-tag--light-outline { color: var(--td-brand-color); }
.csk-root .t-tag--success { background-color: var(--td-success-color); }
.csk-root .t-tag--success.t-tag--outline { border-color: var(--td-success-color); }
.csk-root .t-tag--success.t-tag--light { background-color: var(--td-success-color-light); }
.csk-root .t-tag--success.t-tag--light-outline { border-color: var(--td-success-color); background-color: var(--td-success-color-light); }
.csk-root .t-tag--success.t-tag--light,
.csk-root .t-tag--success.t-tag--outline,
.csk-root .t-tag--success.t-tag--light-outline { color: var(--td-success-color); }
.csk-root .t-tag--warning { background-color: var(--td-warning-color); }
.csk-root .t-tag--warning.t-tag--outline { border-color: var(--td-warning-color); }
.csk-root .t-tag--warning.t-tag--light { background-color: var(--td-warning-color-light); }
.csk-root .t-tag--warning.t-tag--light-outline { border-color: var(--td-warning-color); background-color: var(--td-warning-color-light); }
.csk-root .t-tag--warning.t-tag--light,
.csk-root .t-tag--warning.t-tag--outline,
.csk-root .t-tag--warning.t-tag--light-outline { color: var(--td-warning-color); }
.csk-root .t-tag--danger { background-color: var(--td-error-color); }
.csk-root .t-tag--danger.t-tag--outline { border-color: var(--td-error-color); }
.csk-root .t-tag--danger.t-tag--light { background-color: var(--td-error-color-light); }
.csk-root .t-tag--danger.t-tag--light-outline { border-color: var(--td-error-color); background-color: var(--td-error-color-light); }
.csk-root .t-tag--danger .t-tag__icon-close { fill: var(--td-error-color); }
.csk-root .t-tag--danger .t-tag__icon-close:hover { fill: var(--td-error-color-hover); }
.csk-root .t-tag--danger.t-tag--light,
.csk-root .t-tag--danger.t-tag--outline,
.csk-root .t-tag--danger.t-tag--light-outline { color: var(--td-error-color); }
.csk-root .t-tag.t-tag--outline { background: transparent; }
.csk-root .t-tag.t-size-s { padding: 0px var(--td-comp-paddingLR-xs); height: var(--td-comp-size-xxs); font: var(--td-font-body-small); }
.csk-root .t-tag.t-size-s .t-icon { font-size: var(--td-font-body-small); }
.csk-root .t-tag.t-size-l { padding: 0px var(--td-comp-paddingLR-m); height: var(--td-comp-size-m); font: var(--td-font-body-medium); }
.csk-root .t-tag.t-size-l .t-icon { font: var(--td-font-body-medium); }
.csk-root .t-tag.t-tag--round { border-radius: var(--td-radius-extraLarge); }
.csk-root .t-tag.t-tag--mark { border-radius: 0 var(--td-radius-extraLarge) var(--td-radius-extraLarge) 0; }
.csk-root .t-tag.t-tag--ellipsis .t-tag--text { display: inline-block; white-space: nowrap; word-wrap: normal; overflow: hidden; text-overflow: ellipsis; }
.csk-root .t-tag.t-tag--check:not(.t-tag--disabled) { transition: background-color 0.2s cubic-bezier(0.38, 0, 0.24, 1); cursor: pointer; }
.csk-root .t-tag.t-tag--check:not(.t-tag--disabled).t-tag--default.t-tag--dark:not(.t-tag--checked):hover { background-color: var(--td-bg-color-component-hover); }
.csk-root .t-tag.t-tag--check:not(.t-tag--disabled).t-tag--primary.t-tag--dark.t-tag--checked:hover { background-color: var(--td-brand-color-hover); }
.csk-root .t-tag.t-tag--check:not(.t-tag--disabled).t-tag--default.t-tag--outline:not(.t-tag--checked):hover { color: var(--td-brand-color-hover); }
.csk-root .t-tag--check.t-tag--disabled.t-tag--primary.t-tag--dark { background-color: var(--td-brand-color-disabled); cursor: not-allowed; }
.csk-root .t-tag--check.t-tag--disabled.t-tag--primary.t-tag--outline { color: var(--td-text-color-disabled); background-color: var(--td-bg-color-component-disabled); border-color: var(--td-text-color-disabled); cursor: not-allowed; }
.csk-root .t-check-tag-group .t-tag:not(:last-child) { margin-right: var(--td-comp-margin-s); }
.csk-root a .t-tag { cursor: pointer; transition: all 0.2s linear; }
.csk-root a .t-tag:hover { color: var(--td-brand-color); background-color: var(--td-bg-color-component-hover); }
.csk-root a .t-tag:active { color: var(--td-brand-color-active); background-color: var(--td-bg-color-component-active); }
/* ========== 悬浮按钮 ========== */
.csk-launcher {
position: fixed;
@ -446,41 +1104,16 @@ function getStyles(config: ResolvedConfig): string {
/* ========== P1: RAG 引用来源卡片 ========== */
.csk-sources {
margin-top: 8px;
max-width: 100%;
font-size: 12px;
}
.csk-sources__body {
margin-top: 6px;
border: 1px solid var(--csk-border);
border-radius: 8px;
overflow: hidden;
font-size: 12px;
max-width: 100%;
background: #fff;
}
.csk-sources__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: var(--csk-bg-ai);
cursor: pointer;
user-select: none;
transition: background 0.15s;
}
.csk-sources__header:hover { background: var(--csk-border); }
.csk-sources__title {
display: flex;
align-items: center;
gap: 5px;
font-weight: 500;
color: #374151;
}
.csk-sources__arrow {
transition: transform 0.2s;
color: #9CA3AF;
font-size: 10px;
}
.csk-sources--collapsed .csk-sources__arrow { transform: rotate(-90deg); }
.csk-sources__body {
border-top: 1px solid var(--csk-border);
padding: 0;
}
.csk-sources--collapsed .csk-sources__body { display: none; }
.csk-source-item {
padding: 6px 10px;
@ -661,7 +1294,7 @@ function getStyles(config: ResolvedConfig): string {
.csk-suggestions__list {
display: flex;
flex-wrap: wrap;
gap: 4px;
gap: 8px;
}
/* ========== 新消息提示按钮 ========== */
@ -809,9 +1442,7 @@ function getStyles(config: ResolvedConfig): string {
.csk-dark .csk-header::after { background: linear-gradient(to bottom, rgba(0,0,0,0.12), transparent); }
.csk-dark .csk-input-area { background: #1A1A2E; border-top-color: var(--csk-border); }
.csk-dark .csk-category-bar { background: #1A1A2E; border-top-color: var(--csk-border); }
.csk-dark .csk-sources { background: #1E1E2E; border-color: var(--csk-border); }
.csk-dark .csk-sources__header { background: #25253A; }
.csk-dark .csk-sources__header:hover { background: #2D2D48; }
.csk-dark .csk-sources__body { background: #1E1E2E; border-color: var(--csk-border); }
.csk-dark .csk-source-item:hover { background: var(--csk-bg-ai); }
.csk-dark .csk-source-item__name { color: #E2E8F0; }
.csk-dark .csk-source-item__meta { color: #6B7280; }

21
client/src/utils.ts

@ -40,6 +40,27 @@ export function formatTime(timestamp: number): string {
return `${hh}:${mm}`;
}
/** 格式化历史会话时间:今天/昨天显示 HH:mm,同年显示 MM-DD HH:mm,跨年显示 YYYY-MM-DD HH:mm */
export function formatHistoryTime(timeStr: string | undefined): string {
if (!timeStr) return '';
const d = new Date(timeStr);
if (isNaN(d.getTime())) return timeStr; // 解析失败原样返回,避免破坏展示
const pad = (n: number) => String(n).padStart(2, '0');
const hh = pad(d.getHours());
const mm = pad(d.getMinutes());
const now = new Date();
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
const yesterdayStart = todayStart - 86400000;
const ts = d.getTime();
// 今天或昨天:只显示时分
if (ts >= yesterdayStart) return `${hh}:${mm}`;
const md = `${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
// 今年内:月-日 时分
if (d.getFullYear() === now.getFullYear()) return `${md} ${hh}:${mm}`;
// 跨年:完整年月日 时分
return `${d.getFullYear()}-${md} ${hh}:${mm}`;
}
/** 获取当前时间戳(毫秒) */
export function now(): number {
return Date.now();

2
src/main/resources/static/sdk/test.html

@ -11,8 +11,8 @@
<link rel="modulepreload" crossorigin href="/assets/markdown-DYDVv77n.js">
<link rel="modulepreload" crossorigin href="/assets/chatAdapter-iiOpcNx_.js">
<link rel="stylesheet" crossorigin href="/assets/tdesign-CY0HVqZ3.css">
<link rel="stylesheet" crossorigin href="/assets/tdesign-web-components-B-ycfzW_.css">
<link rel="stylesheet" crossorigin href="/assets/tdesign-chat-Dj1Q23QO.css">
<link rel="stylesheet" crossorigin href="/assets/tdesign-web-components-B-ycfzW_.css">
<link rel="stylesheet" crossorigin href="/assets/sdk-test-nsiAALas.css">
</head>
<body>

Loading…
Cancel
Save