Commit a8eb0ff1 by luoqi

feat(web): 助手隐藏语音输入 + 右上角加最大化

- 语音输入(麦克风)暂时隐藏:VOICE_INPUT_ENABLED=false 门控按钮,听写链路代码保留待恢复。
- 助手小窗头部加「最大化/还原」按钮(Maximize2Minimize2):点击在 400×620 吸附窗 
  1000×634 居中大窗间切换(inset-0 m-auto 定宽高居中);收起时自动还原为小窗。
  本地实测:麦克风不渲染;最大化后面板 1000×634 居中、图标切还原,链路通。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parent 0ce83421
Pipeline #3384 failed in 0 seconds
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
LayoutTemplate, LayoutTemplate,
Loader2, Loader2,
Maximize2, Maximize2,
Minimize2,
Mic, Mic,
Send, Send,
Sparkles, Sparkles,
...@@ -39,6 +40,9 @@ const MODELS = [ ...@@ -39,6 +40,9 @@ const MODELS = [
{ value: 'qwen', label: '通义千问' }, { value: 'qwen', label: '通义千问' },
]; ];
// 语音输入(听写)暂时隐藏 —— 置 true 即恢复麦克风按钮(听写链路代码保留)。
const VOICE_INPUT_ENABLED = false;
const EXAMPLES = [ const EXAMPLES = [
'今日推荐:挑几个该优先跟进的患者,并说明理由', '今日推荐:挑几个该优先跟进的患者,并说明理由',
'查一下患者孙柯的信息、就诊事实和画像', '查一下患者孙柯的信息、就诊事实和画像',
...@@ -419,11 +423,17 @@ function ModelSelect({ ...@@ -419,11 +423,17 @@ function ModelSelect({
export function AssistantChat({ export function AssistantChat({
variant = 'page', variant = 'page',
onClose, onClose,
onToggleMaximize,
maximized = false,
examples, examples,
}: { }: {
/** page = /assistant 整页;widget = 详情页右下角吸附小窗(紧凑头部 + 可收起) */ /** page = /assistant 整页;widget = 详情页右下角吸附小窗(紧凑头部 + 可收起) */
variant?: 'page' | 'widget'; variant?: 'page' | 'widget';
onClose?: () => void; onClose?: () => void;
/** widget:最大化/还原切换(不传则不显示最大化按钮) */
onToggleMaximize?: () => void;
/** widget:当前是否最大化(控制图标 Maximize2↔Minimize2) */
maximized?: boolean;
/** 空态开场建议(规则生成,调用方按场景定制);不传用页面版默认 */ /** 空态开场建议(规则生成,调用方按场景定制);不传用页面版默认 */
examples?: string[]; examples?: string[];
} = {}) { } = {}) {
...@@ -578,6 +588,16 @@ export function AssistantChat({ ...@@ -578,6 +588,16 @@ export function AssistantChat({
</h1> </h1>
<div className="ml-auto flex items-center gap-1.5"> <div className="ml-auto flex items-center gap-1.5">
<ModelSelect model={model} setModel={setModel} disabled={status === 'streaming'} /> <ModelSelect model={model} setModel={setModel} disabled={status === 'streaming'} />
{onToggleMaximize && (
<button
type="button"
onClick={onToggleMaximize}
title={maximized ? '还原' : '最大化'}
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-slate-400 hover:bg-slate-100 hover:text-slate-600"
>
{maximized ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
</button>
)}
{onClose && ( {onClose && (
<button <button
type="button" type="button"
...@@ -650,6 +670,7 @@ export function AssistantChat({ ...@@ -650,6 +670,7 @@ export function AssistantChat({
placeholder="问关于患者画像 / 事实 / 召回计划的问题… (Enter 发送,Shift+Enter 换行)" placeholder="问关于患者画像 / 事实 / 召回计划的问题… (Enter 发送,Shift+Enter 换行)"
className="max-h-[140px] flex-1 resize-none bg-transparent py-1 text-[13.5px] leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none" className="max-h-[140px] flex-1 resize-none bg-transparent py-1 text-[13.5px] leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none"
/> />
{VOICE_INPUT_ENABLED && (
<button <button
type="button" type="button"
onClick={toggleVoice} onClick={toggleVoice}
...@@ -663,6 +684,7 @@ export function AssistantChat({ ...@@ -663,6 +684,7 @@ export function AssistantChat({
> >
<Mic className="h-4 w-4" /> <Mic className="h-4 w-4" />
</button> </button>
)}
{status === 'streaming' ? ( {status === 'streaming' ? (
<button <button
type="button" type="button"
......
...@@ -19,6 +19,7 @@ import { AssistantChat } from './assistant-chat'; ...@@ -19,6 +19,7 @@ import { AssistantChat } from './assistant-chat';
export function AssistantWidget() { export function AssistantWidget() {
const allowed = useHasPermission(Permission.AGENT_INVOKE); const allowed = useHasPermission(Permission.AGENT_INVOKE);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [maximized, setMaximized] = useState(false); // 最大化:居中放大窗
// 窗口落点 + 尺寸:打开瞬间按"钮的当前位置"计算(拖钮的意义所在),并夹紧在视窗内 // 窗口落点 + 尺寸:打开瞬间按"钮的当前位置"计算(拖钮的意义所在),并夹紧在视窗内
const [winRect, setWinRect] = useState<{ x: number; y: number; w: number; h: number } | null>(null); const [winRect, setWinRect] = useState<{ x: number; y: number; w: number; h: number } | null>(null);
...@@ -52,16 +53,26 @@ export function AssistantWidget() { ...@@ -52,16 +53,26 @@ export function AssistantWidget() {
<> <>
{/* 吸附窗:常驻挂载,关着时 invisible(状态不丢) */} {/* 吸附窗:常驻挂载,关着时 invisible(状态不丢) */}
<div <div
style={winRect ? { left: winRect.x, top: winRect.y } : undefined} style={maximized ? undefined : winRect ? { left: winRect.x, top: winRect.y } : undefined}
className={cn( className={cn(
'fixed z-[60] flex flex-col overflow-hidden rounded-xl border border-slate-100 shadow-2xl', 'fixed z-[60] flex flex-col overflow-hidden rounded-xl border border-slate-100 shadow-2xl',
!winRect && 'bottom-4 right-4',
'h-[620px] max-h-[78vh] w-[400px] max-w-[calc(100vw-2rem)]',
'transition-[opacity,transform] duration-150', 'transition-[opacity,transform] duration-150',
maximized
? 'inset-0 m-auto h-[min(88vh,900px)] w-[min(1000px,94vw)]' // 居中放大(margin:auto 定宽高居中)
: cn(!winRect && 'bottom-4 right-4', 'h-[620px] max-h-[78vh] w-[400px] max-w-[calc(100vw-2rem)]'),
open ? 'translate-y-0 opacity-100' : 'pointer-events-none invisible translate-y-2 opacity-0', open ? 'translate-y-0 opacity-100' : 'pointer-events-none invisible translate-y-2 opacity-0',
)} )}
> >
<AssistantChat variant="widget" onClose={() => setOpen(false)} examples={examples} /> <AssistantChat
variant="widget"
onClose={() => {
setOpen(false);
setMaximized(false); // 收起时还原尺寸,下次打开仍是小窗
}}
onToggleMaximize={() => setMaximized((m) => !m)}
maximized={maximized}
examples={examples}
/>
</div> </div>
{/* 助手入口(静态版):固定右下角,点击开/关助手窗;只在 AI 思考/说话、成约、切患者时原地反馈。 {/* 助手入口(静态版):固定右下角,点击开/关助手窗;只在 AI 思考/说话、成约、切患者时原地反馈。
完整桌宠(漫游/物理/狩猎)保留在 /pet-lab 演示页,不进工作区(老板反馈"做得太过")。 */} 完整桌宠(漫游/物理/狩猎)保留在 /pet-lab 演示页,不进工作区(老板反馈"做得太过")。 */}
......
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