feat: 对话框上方集成专家选择器,支持实时选择与系统提示词注入
- 新建 ExpertSelector 组件,加载时查询 session 选中状态,失败降级显示无专家 - 下拉面板含无专家/专家列表/管理专家入口,乐观更新+失败回滚保证一致性 - ChatContainer 在 MessageList 与 MessageInput 之间渲染 ExpertSelector - MessageInput 接收 selectedExpert,选中时 placeholder 显示以专家身份对话 - App.tsx 传递 sessionId/onOpenSettings 给 ChatContainer,支持从对话框直达专家设置 - App.tsx 同时包含 subagent 导航 command 发送的连带修改
This commit is contained in:
parent
5651c4ae7f
commit
61c2fca2a7
@ -166,6 +166,7 @@ function App() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [configPageOpen, setConfigPageOpen] = useState(false)
|
const [configPageOpen, setConfigPageOpen] = useState(false)
|
||||||
|
const [configInitialTab, setConfigInitialTab] = useState<'providers' | 'experts'>('providers')
|
||||||
|
|
||||||
const handleSaveConnection = useCallback((host: string, port: number) => {
|
const handleSaveConnection = useCallback((host: string, port: number) => {
|
||||||
setGatewaySettings({ host, port })
|
setGatewaySettings({ host, port })
|
||||||
@ -355,8 +356,18 @@ function App() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const handleExitSubAgentView = useCallback(() => {
|
const handleExitSubAgentView = useCallback(() => {
|
||||||
exitSubAgentView()
|
const command = exitSubAgentView()
|
||||||
}, [exitSubAgentView])
|
if (command) {
|
||||||
|
sendMessage({ type: 'command', payload: JSON.stringify(command) })
|
||||||
|
}
|
||||||
|
}, [exitSubAgentView, sendMessage])
|
||||||
|
|
||||||
|
const handleNavigateToSubAgentLevel = useCallback((index: number) => {
|
||||||
|
const command = navigateToSubAgentLevel(index)
|
||||||
|
if (command) {
|
||||||
|
sendMessage({ type: 'command', payload: JSON.stringify(command) })
|
||||||
|
}
|
||||||
|
}, [navigateToSubAgentLevel, sendMessage])
|
||||||
|
|
||||||
// 切换到定时任务 tab 时自动获取列表
|
// 切换到定时任务 tab 时自动获取列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -567,7 +578,10 @@ function App() {
|
|||||||
<Brain className="h-4 w-4" />
|
<Brain className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfigPageOpen(true)}
|
onClick={() => {
|
||||||
|
setConfigInitialTab('providers')
|
||||||
|
setConfigPageOpen(true)
|
||||||
|
}}
|
||||||
className="flex h-8 w-8 items-center justify-center rounded-lg text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:bg-[var(--overlay-hover)] transition-all"
|
className="flex h-8 w-8 items-center justify-center rounded-lg text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:bg-[var(--overlay-hover)] transition-all"
|
||||||
title="系统配置"
|
title="系统配置"
|
||||||
aria-label="System config"
|
aria-label="System config"
|
||||||
@ -726,7 +740,7 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
{/* Breadcrumb: 主会话 */}
|
{/* Breadcrumb: 主会话 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => navigateToSubAgentLevel(-1)}
|
onClick={() => handleNavigateToSubAgentLevel(-1)}
|
||||||
className="flex items-center gap-1 text-sm text-[var(--text-muted)] hover:text-[var(--accent-cyan)] transition-colors shrink-0"
|
className="flex items-center gap-1 text-sm text-[var(--text-muted)] hover:text-[var(--accent-cyan)] transition-colors shrink-0"
|
||||||
title="返回主会话"
|
title="返回主会话"
|
||||||
>
|
>
|
||||||
@ -762,7 +776,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => navigateToSubAgentLevel(idx)}
|
onClick={() => handleNavigateToSubAgentLevel(idx)}
|
||||||
className="flex items-center gap-2 text-sm text-[var(--text-muted)] hover:text-[var(--accent-cyan)] transition-colors min-w-0"
|
className="flex items-center gap-2 text-sm text-[var(--text-muted)] hover:text-[var(--accent-cyan)] transition-colors min-w-0"
|
||||||
>
|
>
|
||||||
<span className="truncate">{level.description}</span>
|
<span className="truncate">{level.description}</span>
|
||||||
@ -793,6 +807,11 @@ function App() {
|
|||||||
showThinking={showThinking}
|
showThinking={showThinking}
|
||||||
viewKey={viewKey}
|
viewKey={viewKey}
|
||||||
highlightedMessageId={highlightedMessageId}
|
highlightedMessageId={highlightedMessageId}
|
||||||
|
sessionId={sessionId}
|
||||||
|
onOpenSettings={() => {
|
||||||
|
setConfigInitialTab('experts')
|
||||||
|
setConfigPageOpen(true)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -881,7 +900,11 @@ function App() {
|
|||||||
|
|
||||||
{/* 系统配置页面 */}
|
{/* 系统配置页面 */}
|
||||||
{configPageOpen && (
|
{configPageOpen && (
|
||||||
<ConfigPage onClose={() => setConfigPageOpen(false)} onSaveConnection={handleSaveConnection} />
|
<ConfigPage
|
||||||
|
onClose={() => setConfigPageOpen(false)}
|
||||||
|
onSaveConnection={handleSaveConnection}
|
||||||
|
initialTab={configInitialTab}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import { MessageList } from './MessageList'
|
import { MessageList } from './MessageList'
|
||||||
import { MessageInput } from './MessageInput'
|
import { MessageInput } from './MessageInput'
|
||||||
|
import { ExpertSelector } from './ExpertSelector'
|
||||||
import type { ChatMessage, Attachment } from '../../types/protocol'
|
import type { ChatMessage, Attachment } from '../../types/protocol'
|
||||||
|
|
||||||
interface ChatContainerProps {
|
interface ChatContainerProps {
|
||||||
@ -15,6 +17,10 @@ interface ChatContainerProps {
|
|||||||
viewKey?: string
|
viewKey?: string
|
||||||
/** 高亮的消息 ID */
|
/** 高亮的消息 ID */
|
||||||
highlightedMessageId?: string | null
|
highlightedMessageId?: string | null
|
||||||
|
/** 当前 session ID,用于专家选择 */
|
||||||
|
sessionId?: string | null
|
||||||
|
/** 打开设置页(用于专家管理入口) */
|
||||||
|
onOpenSettings?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatContainer({
|
export function ChatContainer({
|
||||||
@ -28,12 +34,21 @@ export function ChatContainer({
|
|||||||
showThinking = true,
|
showThinking = true,
|
||||||
viewKey,
|
viewKey,
|
||||||
highlightedMessageId,
|
highlightedMessageId,
|
||||||
|
sessionId,
|
||||||
|
onOpenSettings,
|
||||||
}: ChatContainerProps) {
|
}: ChatContainerProps) {
|
||||||
|
const [selectedExpert, setSelectedExpert] = useState<{ name: string; description: string } | null>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col relative">
|
<div className="flex h-full flex-col relative">
|
||||||
<div className="flex-1 overflow-hidden relative">
|
<div className="flex-1 overflow-hidden relative">
|
||||||
<MessageList messages={messages} onNavigateToSubAgent={onNavigateToSubAgent} showThinking={showThinking} viewKey={viewKey} highlightedMessageId={highlightedMessageId} />
|
<MessageList messages={messages} onNavigateToSubAgent={onNavigateToSubAgent} showThinking={showThinking} viewKey={viewKey} highlightedMessageId={highlightedMessageId} />
|
||||||
</div>
|
</div>
|
||||||
|
<ExpertSelector
|
||||||
|
sessionId={sessionId ?? null}
|
||||||
|
onManageExperts={onOpenSettings}
|
||||||
|
onSelectionChange={setSelectedExpert}
|
||||||
|
/>
|
||||||
<MessageInput
|
<MessageInput
|
||||||
onSend={onSendMessage}
|
onSend={onSendMessage}
|
||||||
onStop={onStop}
|
onStop={onStop}
|
||||||
@ -41,6 +56,7 @@ export function ChatContainer({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
channelName={channelName}
|
channelName={channelName}
|
||||||
|
selectedExpert={selectedExpert}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
247
web/src/components/Chat/ExpertSelector.tsx
Normal file
247
web/src/components/Chat/ExpertSelector.tsx
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
|
import { UserCheck, ChevronDown, Loader2, Settings, Check } from 'lucide-react'
|
||||||
|
import { getSelectedExpert, selectExpert } from '../Settings/ConfigPage'
|
||||||
|
|
||||||
|
interface ExpertItem {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
source: string
|
||||||
|
path?: string
|
||||||
|
body?: string
|
||||||
|
disabled_in_scopes: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectedExpert {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExpertSelectorProps {
|
||||||
|
sessionId: string | null
|
||||||
|
onManageExperts?: () => void
|
||||||
|
onSelectionChange?: (expert: SelectedExpert | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExpertSelector({ sessionId, onManageExperts, onSelectionChange }: ExpertSelectorProps) {
|
||||||
|
const [selectedExpert, setSelectedExpert] = useState<SelectedExpert | null>(null)
|
||||||
|
const [expertList, setExpertList] = useState<ExpertItem[]>([])
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [listLoading, setListLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const listFetchedRef = useRef(false)
|
||||||
|
|
||||||
|
// Load current selection whenever sessionId changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sessionId) {
|
||||||
|
setSelectedExpert(null)
|
||||||
|
onSelectionChange?.(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let cancelled = false
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
getSelectedExpert(sessionId)
|
||||||
|
.then(data => {
|
||||||
|
if (cancelled) return
|
||||||
|
if (data?.expert) {
|
||||||
|
setSelectedExpert({ name: data.expert.name, description: data.expert.description })
|
||||||
|
onSelectionChange?.({ name: data.expert.name, description: data.expert.description })
|
||||||
|
} else {
|
||||||
|
setSelectedExpert(null)
|
||||||
|
onSelectionChange?.(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (cancelled) return
|
||||||
|
// Silent fail: default to no expert
|
||||||
|
setSelectedExpert(null)
|
||||||
|
onSelectionChange?.(null)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setLoading(false)
|
||||||
|
})
|
||||||
|
return () => { cancelled = true }
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [sessionId])
|
||||||
|
|
||||||
|
// Click outside to close dropdown
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
const handler = (e: MouseEvent) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handler)
|
||||||
|
return () => document.removeEventListener('mousedown', handler)
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
const fetchExpertList = useCallback(async () => {
|
||||||
|
setListLoading(true)
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/experts')
|
||||||
|
if (resp.ok) {
|
||||||
|
const data = await resp.json()
|
||||||
|
// Only show enabled experts (disabled_in_scopes.length === 0)
|
||||||
|
const enabled = (data.experts ?? []).filter(
|
||||||
|
(e: ExpertItem) => e.disabled_in_scopes.length === 0
|
||||||
|
)
|
||||||
|
setExpertList(enabled)
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
finally { setListLoading(false) }
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleToggleOpen = () => {
|
||||||
|
const next = !open
|
||||||
|
setOpen(next)
|
||||||
|
if (next && !listFetchedRef.current) {
|
||||||
|
listFetchedRef.current = true
|
||||||
|
fetchExpertList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelect = async (expert: SelectedExpert | null) => {
|
||||||
|
if (!sessionId) return
|
||||||
|
// Optimistic update
|
||||||
|
const prev = selectedExpert
|
||||||
|
setSelectedExpert(expert)
|
||||||
|
onSelectionChange?.(expert)
|
||||||
|
setOpen(false)
|
||||||
|
try {
|
||||||
|
const result = await selectExpert(sessionId, expert?.name ?? null)
|
||||||
|
if (!result.success) {
|
||||||
|
// Revert
|
||||||
|
setSelectedExpert(prev)
|
||||||
|
onSelectionChange?.(prev)
|
||||||
|
setError(result.error || '切换专家失败')
|
||||||
|
setTimeout(() => setError(null), 3000)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setSelectedExpert(prev)
|
||||||
|
onSelectionChange?.(prev)
|
||||||
|
setError('网络错误,切换专家失败')
|
||||||
|
setTimeout(() => setError(null), 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleManage = () => {
|
||||||
|
setOpen(false)
|
||||||
|
onManageExperts?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If sessionId is null, render nothing
|
||||||
|
if (!sessionId) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className="relative shrink-0 px-4 pt-2 pb-0">
|
||||||
|
<div className="max-w-5xl mx-auto flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleToggleOpen}
|
||||||
|
disabled={loading}
|
||||||
|
className="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg border border-[var(--border-color)] bg-[var(--bg-tertiary)]/60 hover:border-[var(--accent-cyan)]/40 hover:bg-[var(--bg-tertiary)] transition-colors text-xs disabled:opacity-50"
|
||||||
|
title={selectedExpert ? `${selectedExpert.name}: ${selectedExpert.description}` : '未选中专家'}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin text-[var(--text-muted)]" />
|
||||||
|
) : (
|
||||||
|
<UserCheck
|
||||||
|
className={`h-3.5 w-3.5 ${selectedExpert ? 'text-[var(--accent-cyan)]' : 'text-[var(--text-muted)]'}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedExpert ? (
|
||||||
|
<span className="flex items-center gap-1 min-w-0">
|
||||||
|
<span className="text-[var(--text-primary)] font-medium truncate max-w-[120px]">
|
||||||
|
{selectedExpert.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-[var(--text-muted)] truncate max-w-[180px]">
|
||||||
|
{selectedExpert.description}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-[var(--text-muted)]">无专家</span>
|
||||||
|
)}
|
||||||
|
<ChevronDown
|
||||||
|
className={`h-3 w-3 text-[var(--text-muted)] transition-transform ${open ? 'rotate-180' : ''}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{error && (
|
||||||
|
<span className="text-xs text-red-400 truncate">{error}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
className="absolute z-30 mt-1 w-72 max-w-[90vw] rounded-xl border border-[var(--border-color)] bg-[var(--bg-secondary)] shadow-2xl backdrop-blur-md overflow-hidden"
|
||||||
|
style={{ left: 'max(1rem, calc(50% - 18rem))' }}
|
||||||
|
>
|
||||||
|
{listLoading && expertList.length === 0 ? (
|
||||||
|
<div className="flex items-center gap-2 px-3 py-3 text-xs text-[var(--text-muted)]">
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" /> 加载中...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* 无专家 option */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleSelect(null)}
|
||||||
|
className="w-full flex items-center gap-2 px-3 py-2 text-left text-sm hover:bg-[var(--bg-hover)] transition-colors"
|
||||||
|
>
|
||||||
|
<UserCheck className="h-4 w-4 text-[var(--text-muted)] shrink-0" />
|
||||||
|
<span className="text-[var(--text-primary)]">无专家</span>
|
||||||
|
{!selectedExpert && (
|
||||||
|
<Check className="h-3.5 w-3.5 text-[var(--accent-cyan)] ml-auto" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{expertList.length > 0 && (
|
||||||
|
<div className="border-t border-[var(--border-color)]">
|
||||||
|
{expertList.map(expert => {
|
||||||
|
const isSelected = selectedExpert?.name === expert.name
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={expert.name}
|
||||||
|
onClick={() => handleSelect({ name: expert.name, description: expert.description })}
|
||||||
|
className="w-full flex items-start gap-2 px-3 py-2 text-left hover:bg-[var(--bg-hover)] transition-colors"
|
||||||
|
>
|
||||||
|
<UserCheck
|
||||||
|
className={`h-4 w-4 shrink-0 mt-0.5 ${isSelected ? 'text-[var(--accent-cyan)]' : 'text-[var(--text-muted)]'}`}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-sm font-medium text-[var(--text-primary)] truncate">
|
||||||
|
{expert.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-[9px] px-1 py-0.5 rounded bg-[var(--bg-tertiary)] text-[var(--text-muted)] uppercase tracking-wider shrink-0">
|
||||||
|
{expert.source}
|
||||||
|
</span>
|
||||||
|
{isSelected && (
|
||||||
|
<Check className="h-3.5 w-3.5 text-[var(--accent-cyan)] ml-auto shrink-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-[var(--text-muted)] truncate mt-0.5">
|
||||||
|
{expert.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="border-t border-[var(--border-color)]">
|
||||||
|
<button
|
||||||
|
onClick={handleManage}
|
||||||
|
className="w-full flex items-center gap-2 px-3 py-2 text-left text-sm text-[var(--text-secondary)] hover:text-[var(--accent-cyan)] hover:bg-[var(--bg-hover)] transition-colors"
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4" />
|
||||||
|
<span>管理专家...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ interface MessageInputProps {
|
|||||||
placeholder?: string
|
placeholder?: string
|
||||||
isReadOnly?: boolean
|
isReadOnly?: boolean
|
||||||
channelName?: string
|
channelName?: string
|
||||||
|
selectedExpert?: { name: string; description: string } | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileAttachment {
|
interface FileAttachment {
|
||||||
@ -33,10 +34,13 @@ export function MessageInput({
|
|||||||
onStop,
|
onStop,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
placeholder = '输入消息...按 / 查看命令',
|
placeholder,
|
||||||
isReadOnly = false,
|
isReadOnly = false,
|
||||||
channelName,
|
channelName,
|
||||||
|
selectedExpert,
|
||||||
}: MessageInputProps) {
|
}: MessageInputProps) {
|
||||||
|
const effectivePlaceholder = placeholder
|
||||||
|
?? (selectedExpert ? `以 ${selectedExpert.name} 专家身份对话...` : '输入消息...按 / 查看命令')
|
||||||
const [content, setContent] = useState('')
|
const [content, setContent] = useState('')
|
||||||
const [attachments, setAttachments] = useState<FileAttachment[]>([])
|
const [attachments, setAttachments] = useState<FileAttachment[]>([])
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
@ -360,7 +364,7 @@ export function MessageInput({
|
|||||||
onChange={(e) => setContent(e.target.value)}
|
onChange={(e) => setContent(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
placeholder={placeholder}
|
placeholder={effectivePlaceholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
rows={1}
|
rows={1}
|
||||||
className="w-full resize-none rounded-xl border border-[var(--border-color)] bg-[var(--bg-tertiary)] px-4 py-3 pr-12 text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus:border-[var(--accent-cyan)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--focus-ring)] disabled:opacity-50 transition-all self-center scrollbar-hide"
|
className="w-full resize-none rounded-xl border border-[var(--border-color)] bg-[var(--bg-tertiary)] px-4 py-3 pr-12 text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus:border-[var(--accent-cyan)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--focus-ring)] disabled:opacity-50 transition-all self-center scrollbar-hide"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user