Commit f4e11f94 authored by AI-甘富林's avatar AI-甘富林

feat(ui): refine conversation workspace layout and expert sidebar

parent 6008eeb5
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import brandIcon from "./assets/brand-icon.png"; import brandIcon from "./assets/brand-icon.png";
import type { ReactNode, ChangeEvent, DragEvent as ReactDragEvent, KeyboardEvent as ReactKeyboardEvent } from "react"; import type { ReactNode, CSSProperties, ChangeEvent, DragEvent as ReactDragEvent, KeyboardEvent as ReactKeyboardEvent, PointerEvent as ReactPointerEvent } from "react";
import type { import type {
AppConfig, AppConfig,
ChatAttachment, ChatAttachment,
...@@ -138,6 +138,30 @@ const IMAGE_ATTACHMENT_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", " ...@@ -138,6 +138,30 @@ const IMAGE_ATTACHMENT_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", "
const DOCUMENT_ATTACHMENT_EXTENSIONS = new Set([".pdf", ".ppt", ".pptx", ".xls", ".xlsx", ".csv", ".tsv", ".doc", ".docx", ".txt", ".md", ".json", ".mp3"]); const DOCUMENT_ATTACHMENT_EXTENSIONS = new Set([".pdf", ".ppt", ".pptx", ".xls", ".xlsx", ".csv", ".tsv", ".doc", ".docx", ".txt", ".md", ".json", ".mp3"]);
const SUPPORTED_ATTACHMENT_EXTENSIONS = new Set([...IMAGE_ATTACHMENT_EXTENSIONS, ...DOCUMENT_ATTACHMENT_EXTENSIONS]); const SUPPORTED_ATTACHMENT_EXTENSIONS = new Set([...IMAGE_ATTACHMENT_EXTENSIONS, ...DOCUMENT_ATTACHMENT_EXTENSIONS]);
const COMPOSER_ATTACHMENT_ACCEPT = [...SUPPORTED_ATTACHMENT_EXTENSIONS].join(","); const COMPOSER_ATTACHMENT_ACCEPT = [...SUPPORTED_ATTACHMENT_EXTENSIONS].join(",");
const COMPOSER_TEXTAREA_DEFAULT_MIN_HEIGHT = 48;
const COMPOSER_TEXTAREA_SAFE_MIN_HEIGHT = 38;
const COMPOSER_TEXTAREA_MAX_HEIGHT = 188;
const COMPOSER_TEXTAREA_DEFAULT_RATIO = 0.145;
const COMPOSER_TEXTAREA_MIN_RATIO = 0.12;
const COMPOSER_TEXTAREA_MAX_RATIO = 0.32;
function getComposerTextareaBounds(workspaceHeight: number): { min: number; max: number } {
const safeWorkspaceHeight = Number.isFinite(workspaceHeight) && workspaceHeight > 0 ? workspaceHeight : 0;
const minByWorkspace = safeWorkspaceHeight > 0
? Math.min(COMPOSER_TEXTAREA_DEFAULT_MIN_HEIGHT, safeWorkspaceHeight * COMPOSER_TEXTAREA_MIN_RATIO)
: COMPOSER_TEXTAREA_DEFAULT_MIN_HEIGHT;
const maxByWorkspace = safeWorkspaceHeight * COMPOSER_TEXTAREA_MAX_RATIO;
const dynamicMinHeight = Math.max(COMPOSER_TEXTAREA_SAFE_MIN_HEIGHT, minByWorkspace);
return {
min: dynamicMinHeight,
max: Math.max(dynamicMinHeight, Math.min(COMPOSER_TEXTAREA_MAX_HEIGHT, maxByWorkspace || COMPOSER_TEXTAREA_MAX_HEIGHT))
};
}
function clampComposerTextareaHeight(height: number, workspaceHeight: number): number {
const bounds = getComposerTextareaBounds(workspaceHeight);
return Math.min(bounds.max, Math.max(bounds.min, height));
}
function shouldOfferHomeExpertSwitch(prompt: string): boolean { function shouldOfferHomeExpertSwitch(prompt: string): boolean {
const normalized = prompt.normalize("NFKC").toLowerCase(); const normalized = prompt.normalize("NFKC").toLowerCase();
...@@ -520,32 +544,38 @@ function NavIcon({ kind }: { kind: "chat" | "experts" | "plugins" | "settings" | ...@@ -520,32 +544,38 @@ function NavIcon({ kind }: { kind: "chat" | "experts" | "plugins" | "settings" |
case "chat": case "chat":
return ( return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"> <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M6 6.75A2.75 2.75 0 0 1 8.75 4h6.5A2.75 2.75 0 0 1 18 6.75v4.5A2.75 2.75 0 0 1 15.25 14H12l-3.8 3.15c-.49.41-1.2.06-1.2-.58V14.9A2.75 2.75 0 0 1 6 12.25v-5.5Z" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" /> <path d="M5.25 7.1A2.85 2.85 0 0 1 8.1 4.25h6.2a2.85 2.85 0 0 1 2.85 2.85v3.8a2.85 2.85 0 0 1-2.85 2.85h-2.15l-3.46 2.82c-.5.41-1.25.05-1.25-.6v-2.27A2.85 2.85 0 0 1 5.25 10.9V7.1Z" fill="#CCFBF1" stroke="#0F766E" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.45" />
<path d="M11.55 9.15h5.2A2.25 2.25 0 0 1 19 11.4v2.75a2.25 2.25 0 0 1-2.25 2.25h-.9v1.32c0 .45-.52.7-.87.42L12.8 16.4h-1.25a2.25 2.25 0 0 1-2.25-2.25V11.4a2.25 2.25 0 0 1 2.25-2.25Z" fill="#DBEAFE" stroke="#2563EB" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.35" />
</svg> </svg>
); );
case "experts": case "experts":
return ( return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"> <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="m12 3.75 1.9 3.85 4.25.62-3.08 3 .73 4.23L12 13.52 8.2 15.45l.73-4.23-3.08-3 4.25-.62L12 3.75Z" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" /> <path d="m12 3.75 1.9 3.85 4.25.62-3.08 3 .73 4.23L12 13.52 8.2 15.45l.73-4.23-3.08-3 4.25-.62L12 3.75Z" fill="#EEF2FF" stroke="#6366F1" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.55" />
<path d="m12 7.1.78 1.58 1.75.25-1.27 1.24.3 1.74L12 11.09l-1.56.82.3-1.74-1.27-1.24 1.75-.25L12 7.1Z" fill="#F59E0B" />
</svg> </svg>
); );
case "plugins": case "plugins":
return ( return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"> <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M9.25 4.75h2.5v3h3V4.5a1.75 1.75 0 1 1 3.5 0v3.4a2.1 2.1 0 0 1-2.1 2.1h-2.4v2.25H16a2 2 0 0 1 2 2V17a2.25 2.25 0 0 1-2.25 2.25H13.5v-2.5h-3v2.5H8.25A2.25 2.25 0 0 1 6 17v-2.75a2 2 0 0 1 2-2h2.25V10H7.9a2.1 2.1 0 0 1-2.1-2.1V4.5a1.75 1.75 0 1 1 3.5 0v3.25h3v-3Z" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.7" /> <path d="M9.1 4.5h3.15v3.15h2.45V5.42a1.67 1.67 0 1 1 3.35 0V8a2 2 0 0 1-2 2H12.2v2.2H9.1V9.95H6.95A1.95 1.95 0 0 1 5 8V5.42a1.67 1.67 0 1 1 3.35 0V7.65h.75V4.5Z" fill="#E0E7FF" stroke="#6366F1" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.35" />
<path d="M11.8 12.2h3.1v2.25h.75v-2.22a1.67 1.67 0 1 1 3.35 0v2.57a1.95 1.95 0 0 1-1.95 1.95H14.9v2.75h-3.15v-3.15H9.3v2.23a1.67 1.67 0 1 1-3.35 0V16a2 2 0 0 1 2-2h3.85v-1.8Z" fill="#F3E8FF" stroke="#7C3AED" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.35" />
</svg> </svg>
); );
case "knowledge": case "knowledge":
return ( return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"> <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" <path d="M6 4.5A2.5 2.5 0 0 1 8.5 2h7.1L20 6.4v11.1a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 6 17.5v-13Z" fill="#DBEAFE" stroke="#2563EB" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.45" />
fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" /> <path d="M15.45 2.2v3.1c0 .82.66 1.48 1.48 1.48h2.92" fill="#BFDBFE" stroke="#60A5FA" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.35" />
<path d="M9.1 10.2h5.8M9.1 13.15h4.25" fill="none" stroke="#1D4ED8" strokeLinecap="round" strokeWidth="1.35" />
<path d="m16.7 12.55.38.8.82.12-.6.58.15.82-.75-.4-.75.4.15-.82-.6-.58.82-.12.38-.8Z" fill="#F59E0B" />
</svg> </svg>
); );
case "settings": case "settings":
return ( return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"> <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M12 8.5A3.5 3.5 0 1 1 12 15.5 3.5 3.5 0 0 1 12 8.5Zm7 3.5-.88-.32a1.6 1.6 0 0 1-.97-2.08l.34-.86-1.7-1.7-.86.34a1.6 1.6 0 0 1-2.08-.97L12.5 5h-1l-.32.88a1.6 1.6 0 0 1-2.08.97l-.86-.34-1.7 1.7.34.86a1.6 1.6 0 0 1-.97 2.08L5 12v1l.88.32a1.6 1.6 0 0 1 .97 2.08l-.34.86 1.7 1.7.86-.34a1.6 1.6 0 0 1 2.08.97l.32.88h1l.32-.88a1.6 1.6 0 0 1 2.08-.97l.86.34 1.7-1.7-.34-.86a1.6 1.6 0 0 1 .97-2.08L19 13v-1Z" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.7" /> <path d="M12 8.35A3.65 3.65 0 1 1 12 15.65 3.65 3.65 0 0 1 12 8.35Zm7.2 3.38-.9-.32a1.55 1.55 0 0 1-.94-2.02l.34-.88-1.78-1.78-.88.35a1.55 1.55 0 0 1-2.02-.95L12.68 5h-1.36l-.34 1.13a1.55 1.55 0 0 1-2.02.95l-.88-.35L6.3 8.51l.34.88a1.55 1.55 0 0 1-.94 2.02l-.9.32v1.54l.9.32a1.55 1.55 0 0 1 .94 2.02l-.34.88 1.78 1.78.88-.35a1.55 1.55 0 0 1 2.02.95l.34 1.13h1.36l.34-1.13a1.55 1.55 0 0 1 2.02-.95l.88.35 1.78-1.78-.34-.88a1.55 1.55 0 0 1 .94-2.02l.9-.32v-1.54Z" fill="#CFFAFE" stroke="#0891B2" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.35" />
<circle cx="12" cy="12" r="1.85" fill="#2563EB" />
</svg> </svg>
); );
} }
...@@ -674,34 +704,70 @@ const ui = { ...@@ -674,34 +704,70 @@ const ui = {
const CATEGORY_CONFIG = [ const CATEGORY_CONFIG = [
{ {
id: 'content', id: 'content',
name: '内容营销', name: '内容营销'
icon: '📝',
color: 'rgba(109, 93, 252, 0.2)',
hoverColor: 'rgba(109, 93, 252, 0.3)'
}, },
{ {
id: 'acquisition', id: 'acquisition',
name: '精准获客', name: '精准获客'
icon: '🎯',
color: 'rgba(141, 156, 255, 0.2)',
hoverColor: 'rgba(141, 156, 255, 0.3)'
}, },
{ {
id: 'sales', id: 'sales',
name: '销售冠军', name: '销售冠军'
icon: '🏆',
color: 'rgba(86, 205, 255, 0.2)',
hoverColor: 'rgba(86, 205, 255, 0.3)'
}, },
{ {
id: 'other', id: 'other',
name: '其他专家', name: '其他专家'
icon: '📁',
color: 'rgba(168, 157, 255, 0.2)',
hoverColor: 'rgba(168, 157, 255, 0.3)'
} }
] as const; ] as const;
type ExpertCategoryId = typeof CATEGORY_CONFIG[number]["id"];
function ExpertCategoryIcon({ kind }: { kind: ExpertCategoryId }) {
switch (kind) {
case "content":
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M5.4 5.4A2.4 2.4 0 0 1 7.8 3h7.05L19 7.15v10.45a2.4 2.4 0 0 1-2.4 2.4H7.8a2.4 2.4 0 0 1-2.4-2.4V5.4Z" fill="#F5F3FF" stroke="#7C3AED" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.35" />
<path d="M14.7 3.2v2.75c0 .78.63 1.41 1.41 1.41h2.7" fill="#EDE9FE" stroke="#A78BFA" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.25" />
<path d="M8.5 10.05h6.35M8.5 13h4.55" fill="none" stroke="#6D5DFC" strokeLinecap="round" strokeWidth="1.35" />
<circle cx="15.85" cy="14.85" r="1.55" fill="#F97316" />
<path d="M15.85 13.95v1.8M14.95 14.85h1.8" stroke="#FFF7ED" strokeLinecap="round" strokeWidth="1" />
</svg>
);
case "acquisition":
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="7.3" fill="#ECFEFF" stroke="#06B6D4" strokeWidth="1.35" />
<circle cx="12" cy="12" r="4.45" fill="#DBEAFE" stroke="#2563EB" strokeWidth="1.25" />
<circle cx="12" cy="12" r="1.72" fill="#22C55E" stroke="#F0FDF4" strokeWidth="0.8" />
<path d="M12 3.7v2.2M20.3 12h-2.2M12 20.3v-2.2M3.7 12h2.2" fill="none" stroke="#0F766E" strokeLinecap="round" strokeWidth="1.25" />
<path d="m17.3 5.35 1.25-.8-.35 1.45 1.4.48-1.42.43.3 1.47-1.2-.86-1.1.98.2-1.46-1.45-.32 1.36-.57-.47-1.4 1.25.78.23-.18Z" fill="#F59E0B" />
</svg>
);
case "sales":
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M8 5.15h8v2.2a4 4 0 0 1-3 3.88v1.92h2.15a1.35 1.35 0 0 1 1.35 1.35V16H7.5v-1.5a1.35 1.35 0 0 1 1.35-1.35H11v-1.92a4 4 0 0 1-3-3.88v-2.2Z" fill="#FEF3C7" stroke="#D97706" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.35" />
<path d="M8 6.05H5.85A1.6 1.6 0 0 0 4.25 7.65c0 1.72 1.39 3.1 3.1 3.1H8m8-4.7h2.15a1.6 1.6 0 0 1 1.6 1.6c0 1.72-1.39 3.1-3.1 3.1H16" fill="#FFFBEB" stroke="#F59E0B" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.25" />
<path d="M8.75 16h6.5v3.1h-6.5z" fill="#EDE9FE" stroke="#7C3AED" strokeLinejoin="round" strokeWidth="1.2" />
<path d="M9.75 20h4.5" stroke="#7C3AED" strokeLinecap="round" strokeWidth="1.35" />
<path d="m12 7.15.58 1.16 1.28.19-.93.9.22 1.27L12 10.07l-1.15.6.22-1.27-.93-.9 1.28-.19L12 7.15Z" fill="#EF4444" />
</svg>
);
case "other":
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M4.2 7.7A2.2 2.2 0 0 1 6.4 5.5h11.2a2.2 2.2 0 0 1 2.2 2.2v8.1a2.2 2.2 0 0 1-2.2 2.2H6.4a2.2 2.2 0 0 1-2.2-2.2V7.7Z" fill="#EEF2FF" stroke="#4F46E5" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.35" />
<path d="M4.85 9.05h14.3" stroke="#60A5FA" strokeLinecap="round" strokeWidth="1.2" />
<circle cx="7.05" cy="7.35" r="0.72" fill="#22C55E" />
<circle cx="9.45" cy="7.35" r="0.72" fill="#F59E0B" />
<path d="M9 12.35h6M9 15h3.7" stroke="#7C3AED" strokeLinecap="round" strokeWidth="1.25" />
<path d="m16.15 12.1.3.62.68.1-.49.47.12.67-.61-.32-.6.32.11-.67-.49-.47.68-.1.3-.62Z" fill="#EF4444" />
</svg>
);
}
}
const startupCurtainCopy = { const startupCurtainCopy = {
brandTitle: "\u5343\u5320\u00b7\u95ee\u5929", brandTitle: "\u5343\u5320\u00b7\u95ee\u5929",
brandTagline: "START YOUR IDEAS", brandTagline: "START YOUR IDEAS",
...@@ -2112,12 +2178,18 @@ export default function App() { ...@@ -2112,12 +2178,18 @@ export default function App() {
const [sidebarSessionTitles, setSidebarSessionTitles] = useState<Record<string, string>>({}); const [sidebarSessionTitles, setSidebarSessionTitles] = useState<Record<string, string>>({});
const [skillMenuOpen, setSkillMenuOpen] = useState(false); const [skillMenuOpen, setSkillMenuOpen] = useState(false);
const [isComposerDragOver, setIsComposerDragOver] = useState(false); const [isComposerDragOver, setIsComposerDragOver] = useState(false);
const [isComposerResizeActive, setIsComposerResizeActive] = useState(false);
const [composerTextareaRatio, setComposerTextareaRatio] = useState(COMPOSER_TEXTAREA_DEFAULT_RATIO);
const [composerTextareaHeight, setComposerTextareaHeight] = useState(96);
const [composerWorkspaceHeight, setComposerWorkspaceHeight] = useState(0);
const [copiedToken, setCopiedToken] = useState(""); const [copiedToken, setCopiedToken] = useState("");
const activeStreamRef = useRef<ActiveStreamState | null>(null); const activeStreamRef = useRef<ActiveStreamState | null>(null);
const skillMenuRef = useRef<HTMLDivElement | null>(null); const skillMenuRef = useRef<HTMLDivElement | null>(null);
const attachmentInputRef = useRef<HTMLInputElement | null>(null); const attachmentInputRef = useRef<HTMLInputElement | null>(null);
const conversationWorkspaceRef = useRef<HTMLDivElement | null>(null);
const copiedTokenResetRef = useRef<number | null>(null); const copiedTokenResetRef = useRef<number | null>(null);
const composerDragDepthRef = useRef(0); const composerDragDepthRef = useRef(0);
const composerResizeDragRef = useRef<{ startY: number; startHeight: number; workspaceHeight: number } | null>(null);
const startupWarmupRequestedRef = useRef(false); const startupWarmupRequestedRef = useRef(false);
const lastLoadedWorkspacePathRef = useRef<string | null>(null); const lastLoadedWorkspacePathRef = useRef<string | null>(null);
const [streamSmoke, setStreamSmoke] = useState<SmokeStreamSnapshot | null>(null); const [streamSmoke, setStreamSmoke] = useState<SmokeStreamSnapshot | null>(null);
...@@ -2264,6 +2336,38 @@ export default function App() { ...@@ -2264,6 +2336,38 @@ export default function App() {
const isConversationView = viewMode === "chat" || viewMode === "experts"; const isConversationView = viewMode === "chat" || viewMode === "experts";
const showInlineStartupNotice = startupStateActive && hasVisibleConversation && isConversationView; const showInlineStartupNotice = startupStateActive && hasVisibleConversation && isConversationView;
const pluginGroups = useMemo(() => groupPluginsByStatus(workspace?.plugins), [workspace?.plugins]); const pluginGroups = useMemo(() => groupPluginsByStatus(workspace?.plugins), [workspace?.plugins]);
const composerTextareaBounds = useMemo(() => getComposerTextareaBounds(composerWorkspaceHeight), [composerWorkspaceHeight]);
useEffect(() => {
if (!isConversationView) {
return;
}
const workspaceElement = conversationWorkspaceRef.current;
if (!workspaceElement) {
return;
}
const updateComposerHeight = (workspaceHeight: number) => {
const safeWorkspaceHeight = Number.isFinite(workspaceHeight) && workspaceHeight > 0 ? workspaceHeight : 0;
setComposerWorkspaceHeight((currentHeight) => Math.abs(currentHeight - safeWorkspaceHeight) < 0.5 ? currentHeight : safeWorkspaceHeight);
const nextHeight = clampComposerTextareaHeight(safeWorkspaceHeight * composerTextareaRatio, safeWorkspaceHeight);
setComposerTextareaHeight((currentHeight) => Math.abs(currentHeight - nextHeight) < 0.5 ? currentHeight : nextHeight);
};
updateComposerHeight(workspaceElement.getBoundingClientRect().height);
if (!("ResizeObserver" in window)) {
return;
}
const observer = new ResizeObserver((entries) => {
updateComposerHeight(entries[0]?.contentRect.height ?? workspaceElement.getBoundingClientRect().height);
});
observer.observe(workspaceElement);
return () => observer.disconnect();
}, [composerTextareaRatio, isConversationView]);
useEffect(() => { useEffect(() => {
if (viewMode !== "chat" && viewMode !== "experts") { if (viewMode !== "chat" && viewMode !== "experts") {
...@@ -4150,6 +4254,50 @@ export default function App() { ...@@ -4150,6 +4254,50 @@ export default function App() {
} }
} }
function handleComposerResizePointerDown(event: ReactPointerEvent<HTMLDivElement>) {
if (event.button !== 0) {
return;
}
const workspaceHeight = conversationWorkspaceRef.current?.getBoundingClientRect().height ?? composerWorkspaceHeight;
setComposerWorkspaceHeight((currentHeight) => Math.abs(currentHeight - workspaceHeight) < 0.5 ? currentHeight : workspaceHeight);
composerResizeDragRef.current = {
startY: event.clientY,
startHeight: composerTextareaHeight,
workspaceHeight
};
event.currentTarget.setPointerCapture(event.pointerId);
setIsComposerResizeActive(true);
event.preventDefault();
}
function handleComposerResizePointerMove(event: ReactPointerEvent<HTMLDivElement>) {
const resizeState = composerResizeDragRef.current;
if (!resizeState) {
return;
}
const workspaceHeight = conversationWorkspaceRef.current?.getBoundingClientRect().height || resizeState.workspaceHeight;
const nextHeight = clampComposerTextareaHeight(resizeState.startHeight + resizeState.startY - event.clientY, workspaceHeight);
setComposerWorkspaceHeight((currentHeight) => Math.abs(currentHeight - workspaceHeight) < 0.5 ? currentHeight : workspaceHeight);
setComposerTextareaHeight(nextHeight);
setComposerTextareaRatio(nextHeight / Math.max(workspaceHeight, 1));
event.preventDefault();
}
function handleComposerResizePointerEnd(event: ReactPointerEvent<HTMLDivElement>) {
if (!composerResizeDragRef.current) {
return;
}
composerResizeDragRef.current = null;
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
setIsComposerResizeActive(false);
event.preventDefault();
}
async function openAttachmentPicker() { async function openAttachmentPicker() {
if (window.qjcDesktop) { if (window.qjcDesktop) {
const attachments = await desktopApi.chat.pickAttachments(); const attachments = await desktopApi.chat.pickAttachments();
...@@ -4407,6 +4555,11 @@ export default function App() { ...@@ -4407,6 +4555,11 @@ export default function App() {
</button> </button>
); );
const conversationPanelTitle = viewMode === "experts" ? activeExpertName : "对话"; const conversationPanelTitle = viewMode === "experts" ? activeExpertName : "对话";
const expertWorkspaceLogo = viewMode === "experts" && (activeExpertKey === "xiaohongshu" || activeExpertKey === "douyin") ? (
<span className={"expert-workspace-logo expert-workspace-logo-" + activeExpertKey} aria-hidden="true">
{activeExpertKey === "xiaohongshu" ? <RedBookIcon /> : <DouyinNoteIcon />}
</span>
) : null;
const conversationPanelLead = viewMode === "chat" ? ( const conversationPanelLead = viewMode === "chat" ? (
<div className="home-microcopy" aria-label="start your idea"> <div className="home-microcopy" aria-label="start your idea">
<span className="home-microcopy-icon"> <span className="home-microcopy-icon">
...@@ -4417,9 +4570,11 @@ export default function App() { ...@@ -4417,9 +4570,11 @@ export default function App() {
</div> </div>
) : ( ) : (
<div className="conversation-panel-kicker expert-hero-kicker"> <div className="conversation-panel-kicker expert-hero-kicker">
<span className={"expert-hero-icon expert-hero-icon-" + activeExpertVisualKey} aria-hidden="true"> {expertWorkspaceLogo ?? (
{renderExpertIcon(activeExpertVisualKey)} <span className={"expert-hero-icon expert-hero-icon-" + activeExpertVisualKey} aria-hidden="true">
</span> {renderExpertIcon(activeExpertVisualKey)}
</span>
)}
<span className="expert-hero-copy"> <span className="expert-hero-copy">
{/* <span className="expert-hero-label">当前专家</span> */} {/* <span className="expert-hero-label">当前专家</span> */}
<strong>{conversationPanelTitle}</strong> <strong>{conversationPanelTitle}</strong>
...@@ -4689,15 +4844,22 @@ export default function App() { ...@@ -4689,15 +4844,22 @@ export default function App() {
: viewMode === "experts" && !expertPageProjects.length : viewMode === "experts" && !expertPageProjects.length
? <div className="empty-state">{expertsPageCopy.noExperts}</div> ? <div className="empty-state">{expertsPageCopy.noExperts}</div>
: messageListContent; : messageListContent;
const composerShellStyle = {
"--composer-textarea-height": `${Math.round(composerTextareaHeight)}px`,
"--composer-textarea-min-height": `${Math.round(composerTextareaBounds.min)}px`,
"--composer-textarea-max-height": `${Math.round(composerTextareaBounds.max)}px`
} as CSSProperties;
const composerContent = ( const composerContent = (
<form <form
className={ className={
"composer-shell" "composer-shell"
+ (isComposerDragOver ? " dragging" : "") + (isComposerDragOver ? " dragging" : "")
+ (isComposerResizeActive ? " resizing" : "")
+ (viewMode === "chat" ? " composer-shell-home" : "") + (viewMode === "chat" ? " composer-shell-home" : "")
+ (viewMode === "experts" ? " composer-shell-expert" : "") + (viewMode === "experts" ? " composer-shell-expert" : "")
} }
style={composerShellStyle}
onSubmit={(event) => { onSubmit={(event) => {
event.preventDefault(); event.preventDefault();
void sendPrompt(); void sendPrompt();
...@@ -4717,6 +4879,19 @@ export default function App() { ...@@ -4717,6 +4879,19 @@ export default function App() {
onChange={handleAttachmentSelection} onChange={handleAttachmentSelection}
/> />
{isComposerDragOver ? <div className="composer-drop-indicator">释放以上传附件</div> : null} {isComposerDragOver ? <div className="composer-drop-indicator">释放以上传附件</div> : null}
<div
className="composer-resize-handle"
role="separator"
aria-orientation="horizontal"
aria-label={"\u8c03\u6574\u8f93\u5165\u6846\u9ad8\u5ea6"}
title={"\u8c03\u6574\u8f93\u5165\u6846\u9ad8\u5ea6"}
onPointerDown={handleComposerResizePointerDown}
onPointerMove={handleComposerResizePointerMove}
onPointerUp={handleComposerResizePointerEnd}
onPointerCancel={handleComposerResizePointerEnd}
>
<span aria-hidden="true" />
</div>
<div className="composer-surface"> <div className="composer-surface">
<label className="composer-field"> <label className="composer-field">
<textarea <textarea
...@@ -4803,20 +4978,11 @@ export default function App() { ...@@ -4803,20 +4978,11 @@ export default function App() {
<WindowControlIcon kind="close" /> <WindowControlIcon kind="close" />
</button> </button>
</div> </div>
<aside className={"sidebar app-drag-region" + (isConversationView ? " conversation-sidebar-layout" : "")}> <aside className="sidebar conversation-sidebar-layout app-drag-region">
<div className="sidebar-top"> <div className="sidebar-top">
<div className="sidebar-logo-block" aria-label="千匠问天">
<div className="sidebar-logo-mark-shell" aria-hidden="true">
<img src={brandIcon} alt="" className="sidebar-logo-mark" />
</div>
<div className="sidebar-logo-copy">
<strong>千匠问天</strong>
</div>
</div>
<nav className="nav-list"> <nav className="nav-list">
{[ {[
{ id: "chat" as const, label: "对话" }, { id: "chat" as const, label: "对话" },
{ id: "experts" as const, label: "数字员工" },
{ id: "knowledge" as const, label: ui.knowledge }, { id: "knowledge" as const, label: ui.knowledge },
{ id: "plugins" as const, label: ui.plugins }, { id: "plugins" as const, label: ui.plugins },
{ id: "settings" as const, label: ui.settings } { id: "settings" as const, label: ui.settings }
...@@ -4833,6 +4999,11 @@ export default function App() { ...@@ -4833,6 +4999,11 @@ export default function App() {
</div> </div>
<div className="sidebar-bottom"> <div className="sidebar-bottom">
<section className="sidebar-section compact sidebar-experts-entry"> <section className="sidebar-section compact sidebar-experts-entry">
<div className="sidebar-section-head sidebar-digital-workers-title">
<div className="sidebar-section-copy">
<strong className="sidebar-section-title">数字员工</strong>
</div>
</div>
<div className="sidebar-expert-scroll"> <div className="sidebar-expert-scroll">
<div className="expert-category-list"> <div className="expert-category-list">
{CATEGORY_CONFIG.map((category) => { {CATEGORY_CONFIG.map((category) => {
...@@ -4864,18 +5035,25 @@ export default function App() { ...@@ -4864,18 +5035,25 @@ export default function App() {
}); });
const isExpanded = expandedCategories[category.id] || false; const isExpanded = expandedCategories[category.id] || false;
const categoryPanelId = `expert-tree-${category.id}`;
const hasExperts = categoryExperts.length > 0;
return ( return (
<div <div
key={category.id} key={category.id}
className="expert-category-item" className="expert-category-item expert-tree-category"
> >
{/* 分类头部 - 可点击展开/收起 */} <button
<div type="button"
className="expert-category-header" className="expert-category-header expert-tree-category-trigger app-no-drag"
onClick={() => toggleCategory(category.id)} onClick={() => toggleCategory(category.id)}
disabled={!hasExperts}
aria-expanded={isExpanded}
aria-controls={categoryPanelId}
> >
<div className="expert-category-icon">{category.icon}</div> <span className="expert-category-icon" aria-hidden="true">
<ExpertCategoryIcon kind={category.id} />
</span>
<div className="expert-category-title"> <div className="expert-category-title">
<div className="expert-category-name">{category.name}</div> <div className="expert-category-name">{category.name}</div>
</div> </div>
...@@ -4884,11 +5062,11 @@ export default function App() { ...@@ -4884,11 +5062,11 @@ export default function App() {
<path d="M6 9l6 6 6-6"/> <path d="M6 9l6 6 6-6"/>
</svg> </svg>
</div> </div>
</div> </button>
{/* 展开的内容区域 */} {/* 展开的内容区域 */}
{isExpanded && categoryExperts.length > 0 && ( {isExpanded && hasExperts && (
<div className="expert-category-content"> <div id={categoryPanelId} className="expert-category-content expert-tree-list">
<div className="expert-category-experts"> <div className="expert-category-experts">
{categoryExperts.map((entry) => { {categoryExperts.map((entry) => {
const expertVisualKey = resolveExpertVisualKey(entry.project, entry.definition); const expertVisualKey = resolveExpertVisualKey(entry.project, entry.definition);
...@@ -4898,16 +5076,17 @@ export default function App() { ...@@ -4898,16 +5076,17 @@ export default function App() {
: viewMode === "chat" && prompt.trim() === buildShortcutPrompt(entry.definition); : viewMode === "chat" && prompt.trim() === buildShortcutPrompt(entry.definition);
return ( return (
<div <button
type="button"
key={entry.definition.id} key={entry.definition.id}
className="expert-category-expert-item" className={"expert-category-expert-item expert-tree-expert app-no-drag" + (isActive ? " active" : "")}
onClick={() => handleExpertSelect(entry)} onClick={() => handleExpertSelect(entry)}
> >
<div className="expert-category-expert-icon"> <div className="expert-category-expert-icon">
{renderExpertIcon(expertVisualKey)} {renderExpertIcon(expertVisualKey)}
</div> </div>
<div className="expert-category-expert-name">{entry.displayName}</div> <div className="expert-category-expert-name">{entry.displayName}</div>
</div> </button>
); );
})} })}
</div> </div>
...@@ -4980,7 +5159,7 @@ export default function App() { ...@@ -4980,7 +5159,7 @@ export default function App() {
{isMockDesktopApi ? <StatusChip tone="warning">Mock API</StatusChip> : null} {isMockDesktopApi ? <StatusChip tone="warning">Mock API</StatusChip> : null}
</div> </div>
</div> </div>
<div className="conversation-workspace"> <div className="conversation-workspace" ref={conversationWorkspaceRef}>
{conversationStatusNotice} {conversationStatusNotice}
{viewMode === "chat" ? homeIntentSuggestionNotice : null} {viewMode === "chat" ? homeIntentSuggestionNotice : null}
<div className="conversation-panel-body"> <div className="conversation-panel-body">
......
...@@ -280,8 +280,10 @@ strong { font-weight: 600; } ...@@ -280,8 +280,10 @@ strong { font-weight: 600; }
color: var(--color-text-secondary); color: var(--color-text-secondary);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px;
font-family: "Microsoft YaHei UI", "PingFang SC", "Segoe UI", sans-serif;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 600;
box-shadow: none; box-shadow: none;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
...@@ -2810,7 +2812,8 @@ button.secondary { ...@@ -2810,7 +2812,8 @@ button.secondary {
} }
.sidebar-section-title { .sidebar-section-title {
font-size: 15px; font-size: 14px;
font-weight: 600;
} }
.sidebar-experts-entry { .sidebar-experts-entry {
...@@ -2834,20 +2837,19 @@ button.secondary { ...@@ -2834,20 +2837,19 @@ button.secondary {
.expert-category-list { .expert-category-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 6px;
} }
.expert-category-item { .expert-category-item {
border-radius: 16px; border-radius: 14px;
background: linear-gradient(135deg, background: linear-gradient(135deg,
rgba(235, 240, 255, 0.9), rgba(235, 240, 255, 0.9),
rgba(245, 240, 255, 0.9) rgba(245, 240, 255, 0.9)
); );
border: 1px solid rgba(109, 93, 252, 0.15); border: 1px solid rgba(109, 93, 252, 0.15);
box-shadow: 0 4px 12px rgba(109, 93, 252, 0.08); box-shadow: 0 4px 12px rgba(109, 93, 252, 0.08);
transition: all 0.2s ease; overflow: hidden;
position: relative; transition: border-color 180ms ease, box-shadow 180ms ease, background 180ms ease;
margin-bottom: 4px;
} }
.expert-category-item:hover { .expert-category-item:hover {
...@@ -2856,13 +2858,21 @@ button.secondary { ...@@ -2856,13 +2858,21 @@ button.secondary {
} }
.expert-category-header { .expert-category-header {
width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: 8px;
padding: 6px 16px; min-height: 40px;
padding: 0 10px;
border: 0;
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer; cursor: pointer;
box-shadow: none;
user-select: none; user-select: none;
min-height: 40px; /* 进一步缩小高度 */ text-align: left;
transition: background 180ms ease, color 180ms ease;
} }
.expert-category-header:hover { .expert-category-header:hover {
...@@ -2870,39 +2880,53 @@ button.secondary { ...@@ -2870,39 +2880,53 @@ button.secondary {
rgba(235, 240, 255, 0.95), rgba(235, 240, 255, 0.95),
rgba(245, 240, 255, 0.95) rgba(245, 240, 255, 0.95)
); );
box-shadow: none;
transform: none;
}
.expert-category-header:disabled {
cursor: default;
opacity: 0.58;
}
.expert-category-header:disabled:hover {
background: transparent;
} }
.expert-category-icon { .expert-category-icon {
font-size: 14px; width: 22px;
line-height: 1; height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0; flex-shrink: 0;
color: #6D5DFC;
}
.expert-category-icon svg {
width: 18px;
height: 18px;
} }
.expert-category-title { .expert-category-title {
flex: 1; flex: 0 1 auto;
min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
} }
.expert-category-name { .expert-category-name {
font-size: 12px; font-family: "Microsoft YaHei UI", "PingFang SC", "Segoe UI", sans-serif;
font-size: 14px;
font-weight: 600; font-weight: 600;
color: #4A5568; color: #4A5568;
text-align: left;
line-height: 1.2; line-height: 1.2;
} }
.expert-category-count {
font-size: 10px;
font-weight: 500;
color: #6D5DFC;
background: rgba(109, 93, 252, 0.1);
padding: 2px 8px;
border-radius: 10px;
align-self: flex-start;
}
.expert-category-toggle { .expert-category-toggle {
margin-left: auto;
width: 20px; width: 20px;
height: 20px; height: 20px;
display: flex; display: flex;
...@@ -2918,83 +2942,51 @@ button.secondary { ...@@ -2918,83 +2942,51 @@ button.secondary {
} }
.expert-category-content { .expert-category-content {
position: absolute; position: static;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: linear-gradient(135deg, background: linear-gradient(135deg,
rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.62),
rgba(245, 240, 255, 0.96) rgba(245, 240, 255, 0.58)
); );
border: 1px solid rgba(109, 93, 252, 0.2); border-top: 1px solid rgba(109, 93, 252, 0.12);
border-radius: 16px; box-shadow: none;
box-shadow: margin-top: 0;
0 22px 48px rgba(109, 93, 252, 0.12),
0 8px 20px rgba(109, 93, 252, 0.08);
margin-top: 8px;
animation: slide-down 0.2s ease-out;
overflow: hidden;
}
@keyframes slide-down {
0% {
opacity: 0;
transform: translateY(-8px);
}
100% {
opacity: 1;
transform: translateY(0);
}
} }
.expert-category-experts { .expert-category-experts {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 4px;
padding: 16px; padding: 6px;
max-height: 320px;
overflow-y: auto;
/* 透明滚动条 */
scrollbar-width: thin;
scrollbar-color: rgba(139, 92, 246, 0.3) transparent;
}
.expert-category-experts::-webkit-scrollbar {
width: 4px;
}
.expert-category-experts::-webkit-scrollbar-track {
background: transparent;
border-radius: 2px;
}
.expert-category-experts::-webkit-scrollbar-thumb {
background: rgba(139, 92, 246, 0.3);
border-radius: 2px;
transition: background 0.2s ease;
}
.expert-category-experts::-webkit-scrollbar-thumb:hover {
background: rgba(139, 92, 246, 0.5);
} }
.expert-category-expert-item { .expert-category-expert-item {
width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 8px;
padding: 10px 12px; min-height: 34px;
border-radius: 12px; padding: 6px 8px 6px 28px;
background: rgba(255, 255, 255, 0.7); border-radius: 10px;
border: 1px solid rgba(109, 93, 252, 0.1); border: 1px solid rgba(109, 93, 252, 0.1);
background: rgba(255, 255, 255, 0.7);
color: #4A5568;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; box-shadow: none;
text-align: left;
transition: background 150ms ease, border-color 150ms ease, color 150ms ease;
} }
.expert-category-expert-item:hover { .expert-category-expert-item:hover {
background: rgba(109, 93, 252, 0.05); background: rgba(109, 93, 252, 0.05);
border-color: rgba(109, 93, 252, 0.2); border-color: rgba(109, 93, 252, 0.2);
transform: translateY(-1px); box-shadow: none;
transform: none;
}
.expert-category-expert-item.active {
color: #5b4fe8;
background: rgba(109, 93, 252, 0.1);
border-color: rgba(109, 93, 252, 0.24);
} }
.expert-category-expert-icon { .expert-category-expert-icon {
...@@ -3067,22 +3059,14 @@ button.secondary { ...@@ -3067,22 +3059,14 @@ button.secondary {
} }
.expert-category-name { .expert-category-name {
font-size: 12px; font-family: "Microsoft YaHei UI", "PingFang SC", "Segoe UI", sans-serif;
font-size: 14px;
font-weight: 600; font-weight: 600;
color: #4A5568; color: #4A5568;
text-align: center; text-align: left;
line-height: 1.2; line-height: 1.2;
} }
.expert-category-count {
font-size: 10px;
font-weight: 500;
color: #6D5DFC;
background: rgba(109, 93, 252, 0.1);
padding: 2px 6px;
border-radius: 8px;
}
.expert-chip-list { .expert-chip-list {
gap: 10px; gap: 10px;
} }
...@@ -3134,12 +3118,6 @@ button.secondary { ...@@ -3134,12 +3118,6 @@ button.secondary {
position: relative; position: relative;
} }
.sidebar-expert-scroll,
.sidebar-section,
.sidebar-bottom {
overflow: visible !important;
}
@keyframes popup-fade-in { @keyframes popup-fade-in {
0% { 0% {
opacity: 0; opacity: 0;
...@@ -4768,7 +4746,7 @@ button.secondary { ...@@ -4768,7 +4746,7 @@ button.secondary {
} }
.shell { .shell {
grid-template-columns: 280px minmax(0, 1fr); grid-template-columns: clamp(232px, 19vw, 280px) minmax(0, 1fr);
background: #f0f7ff; background: #f0f7ff;
} }
...@@ -4776,13 +4754,17 @@ button.secondary { ...@@ -4776,13 +4754,17 @@ button.secondary {
display: none; display: none;
} }
.conversation-shell .conversation-sidebar-layout { .conversation-shell {
grid-template-columns: clamp(232px, 19vw, 280px) minmax(0, 1fr);
}
.conversation-sidebar-layout {
grid-template-rows: auto minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr);
gap: 16px; gap: 12px;
} }
.conversation-shell .sidebar-top { .sidebar-top {
gap: 14px; gap: 10px;
} }
.conversation-sidebar-action { .conversation-sidebar-action {
...@@ -4798,47 +4780,95 @@ button.secondary { ...@@ -4798,47 +4780,95 @@ button.secondary {
border-radius: 16px; border-radius: 16px;
} }
.conversation-shell .sidebar-bottom { .sidebar-bottom {
min-height: 0; min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow-y: auto;
overflow-x: hidden;
gap: 12px; gap: 12px;
padding-right: 0; padding-right: 2px;
scrollbar-width: thin;
scrollbar-color: rgba(139, 92, 246, 0.28) transparent;
} }
.conversation-shell .sidebar-experts-entry, .sidebar-bottom::-webkit-scrollbar {
.conversation-shell .sidebar-session-section { width: 4px;
flex: 1 1 0; }
min-height: 0;
.sidebar-bottom::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-bottom::-webkit-scrollbar-thumb {
border-radius: 999px;
background: rgba(139, 92, 246, 0.28);
}
.sidebar-experts-entry,
.sidebar-session-section {
padding: 14px; padding: 14px;
border-radius: 22px; border-radius: 22px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 251, 250, 0.96)); background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 251, 250, 0.96));
box-shadow: 0 18px 34px rgba(17, 24, 39, 0.06); box-shadow: 0 18px 34px rgba(17, 24, 39, 0.06);
} }
.conversation-shell .sidebar-experts-entry { .sidebar-experts-entry {
flex: 0 0 calc(55% - 6px);
min-height: calc(55% - 6px);
display: grid; display: grid;
grid-template-rows: minmax(0, 1fr); grid-template-rows: auto auto;
gap: 10px;
align-content: start;
overflow: visible;
} }
.conversation-shell .sidebar-section-fill { .sidebar-session-section {
flex: 0 0 calc(45% - 6px);
min-height: calc(45% - 6px);
display: grid; display: grid;
grid-template-rows: auto minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr);
overflow: hidden;
}
.sidebar-digital-workers-title {
min-height: 24px;
align-items: center;
position: sticky;
top: 0;
z-index: 1;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 251, 250, 0.96));
} }
.conversation-shell .sidebar-expert-scroll { .sidebar-section-fill {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
}
.sidebar-expert-scroll {
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: visible;
overflow-x: hidden; overflow-x: hidden;
padding-right: 2px; padding-right: 0;
/* 继承自定义滚动条样式 */ /* 继承自定义滚动条样式 */
} }
.conversation-shell .sidebar-session-list { .expert-category-header:focus-visible,
.expert-category-expert-item:focus-visible {
outline: 2px solid rgba(109, 93, 252, 0.38);
outline-offset: 2px;
}
.sidebar-session-list {
min-height: 0; min-height: 0;
overflow: auto; overflow: auto;
padding-right: 2px; padding-right: 2px;
scrollbar-width: thin;
scrollbar-color: rgba(139, 92, 246, 0.3) transparent;
}
.sidebar-session-list::-webkit-scrollbar-thumb {
background: rgba(139, 92, 246, 0.3);
} }
.conversation-shell .conversation-main-layout, .conversation-shell .conversation-main-layout,
...@@ -4881,6 +4911,34 @@ button.secondary { ...@@ -4881,6 +4911,34 @@ button.secondary {
min-width: 0; min-width: 0;
} }
.conversation-shell .expert-workspace-logo {
width: 44px;
height: 44px;
flex: 0 0 auto;
display: grid;
place-items: center;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.72);
background: rgba(255, 255, 255, 0.78);
box-shadow: 0 14px 28px rgba(15, 23, 42, 0.1);
}
.conversation-shell .expert-workspace-logo svg {
width: 26px;
height: 26px;
display: block;
}
.conversation-shell .expert-workspace-logo-xiaohongshu {
background: linear-gradient(135deg, rgba(255, 245, 245, 0.96), rgba(255, 228, 230, 0.9));
box-shadow: 0 14px 28px rgba(239, 68, 68, 0.16);
}
.conversation-shell .expert-workspace-logo-douyin {
background: linear-gradient(135deg, rgba(236, 254, 255, 0.96), rgba(244, 244, 255, 0.92));
box-shadow: 0 14px 28px rgba(109, 125, 255, 0.16);
}
.conversation-shell .conversation-panel-actions { .conversation-shell .conversation-panel-actions {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
...@@ -5022,6 +5080,9 @@ button.secondary { ...@@ -5022,6 +5080,9 @@ button.secondary {
} }
.conversation-shell .composer-shell { .conversation-shell .composer-shell {
--composer-textarea-height: 96px;
--composer-textarea-min-height: 48px;
--composer-textarea-max-height: 188px;
position: relative; position: relative;
gap: 0; gap: 0;
padding: 14px 0 0; padding: 14px 0 0;
...@@ -5035,6 +5096,51 @@ button.secondary { ...@@ -5035,6 +5096,51 @@ button.secondary {
border-top: 1px solid rgba(141, 156, 255, 0.1); /* 浅紫色边框 */ border-top: 1px solid rgba(141, 156, 255, 0.1); /* 浅紫色边框 */
} }
.conversation-shell .composer-shell.resizing,
.conversation-shell .composer-shell.resizing * {
cursor: ns-resize;
}
.conversation-shell .composer-resize-handle {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
border: 0;
background: transparent;
cursor: ns-resize;
touch-action: none;
user-select: none;
z-index: 2;
}
.conversation-shell .composer-resize-handle span {
width: 48px;
height: 4px;
border-radius: 999px;
background: rgba(109, 93, 252, 0.2);
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.82);
opacity: 0.72;
transition: background 160ms ease, opacity 160ms ease, transform 160ms ease;
}
.conversation-shell .composer-resize-handle:hover span,
.conversation-shell .composer-resize-handle:focus-visible span,
.conversation-shell .composer-shell.resizing .composer-resize-handle span {
background: rgba(109, 93, 252, 0.38);
opacity: 1;
transform: scaleX(1.12);
}
.conversation-shell .composer-resize-handle:focus-visible {
outline: 2px solid rgba(109, 93, 252, 0.32);
outline-offset: 2px;
}
.conversation-shell .composer-surface { .conversation-shell .composer-surface {
display: grid; display: grid;
gap: 12px; gap: 12px;
...@@ -5064,8 +5170,9 @@ button.secondary { ...@@ -5064,8 +5170,9 @@ button.secondary {
.conversation-shell .composer-field textarea, .conversation-shell .composer-field textarea,
.conversation-shell .composer-textarea { .conversation-shell .composer-textarea {
min-height: 72px; height: var(--composer-textarea-height);
max-height: 188px; min-height: var(--composer-textarea-min-height);
max-height: var(--composer-textarea-max-height);
padding: 0; padding: 0;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
...@@ -5252,6 +5359,71 @@ button.secondary { ...@@ -5252,6 +5359,71 @@ button.secondary {
padding: 32px; padding: 32px;
} }
@media (max-width: 1440px), (max-height: 820px) {
.shell,
.conversation-shell {
grid-template-columns: clamp(232px, 18vw, 260px) minmax(0, 1fr);
}
.conversation-sidebar-layout {
padding: 14px 12px;
gap: 10px;
}
.sidebar-top {
gap: 8px;
}
.nav-list {
gap: 8px;
}
.nav-item {
height: 40px;
}
.conversation-sidebar-action .conversation-new-session {
min-height: 40px;
padding: 0 12px;
}
.sidebar-bottom {
gap: 10px;
}
.sidebar-experts-entry {
flex-basis: calc(55% - 5px);
min-height: calc(55% - 5px);
padding: 10px;
gap: 8px;
}
.sidebar-session-section {
flex-basis: calc(45% - 5px);
min-height: calc(45% - 5px);
padding: 10px;
}
.expert-category-list {
gap: 4px;
}
.expert-category-header {
min-height: 36px;
padding: 0 8px;
}
.expert-category-expert-item {
min-height: 32px;
padding-top: 5px;
padding-bottom: 5px;
}
.sidebar-session-card {
min-height: 40px;
}
}
@media (max-width: 960px) { @media (max-width: 960px) {
.conversation-shell .conversation-workspace { .conversation-shell .conversation-workspace {
padding: 18px; padding: 18px;
...@@ -5510,7 +5682,6 @@ button.secondary { ...@@ -5510,7 +5682,6 @@ button.secondary {
.nav-item.active { .nav-item.active {
background: rgba(124, 58, 237, 0.15); background: rgba(124, 58, 237, 0.15);
border-left: 3px solid var(--ui-color-primary);
} }
.nav-item.active::before { .nav-item.active::before {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment