add command view
This commit is contained in:
parent
4a5823721e
commit
43599fca47
@ -1,7 +1,7 @@
|
|||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
|
||||||
import { useMutation } from '@tanstack/react-query'
|
import { useMutation } from '@tanstack/react-query'
|
||||||
import { CircleStop, History, Plus } from 'lucide-react'
|
import { CircleStop, History, Plus, SquareSlash } from 'lucide-react'
|
||||||
import { App, Notice } from 'obsidian'
|
import { App, Notice } from 'obsidian'
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
@ -52,6 +52,7 @@ import { ModeSelect } from './chat-input/ModeSelect'
|
|||||||
import PromptInputWithActions, { ChatUserInputRef } from './chat-input/PromptInputWithActions'
|
import PromptInputWithActions, { ChatUserInputRef } from './chat-input/PromptInputWithActions'
|
||||||
import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text'
|
import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text'
|
||||||
import { ChatHistory } from './ChatHistory'
|
import { ChatHistory } from './ChatHistory'
|
||||||
|
import CommandsView from './CommandsView'
|
||||||
import MarkdownReasoningBlock from './Markdown/MarkdownReasoningBlock'
|
import MarkdownReasoningBlock from './Markdown/MarkdownReasoningBlock'
|
||||||
import QueryProgress, { QueryProgressState } from './QueryProgress'
|
import QueryProgress, { QueryProgressState } from './QueryProgress'
|
||||||
import ReactMarkdown from './ReactMarkdown'
|
import ReactMarkdown from './ReactMarkdown'
|
||||||
@ -160,6 +161,8 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [tab, setTab] = useState<'chat' | 'commands'>('commands')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scrollContainer = chatMessagesRef.current
|
const scrollContainer = chatMessagesRef.current
|
||||||
if (!scrollContainer) return
|
if (!scrollContainer) return
|
||||||
@ -870,11 +873,15 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="infio-chat-container">
|
<div className="infio-chat-container">
|
||||||
|
{/* header view */}
|
||||||
<div className="infio-chat-header">
|
<div className="infio-chat-header">
|
||||||
<ModeSelect />
|
<ModeSelect />
|
||||||
<div className="infio-chat-header-buttons">
|
<div className="infio-chat-header-buttons">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleNewChat()}
|
onClick={() => {
|
||||||
|
setTab('chat')
|
||||||
|
handleNewChat()
|
||||||
|
}}
|
||||||
className="infio-chat-list-dropdown"
|
className="infio-chat-list-dropdown"
|
||||||
>
|
>
|
||||||
<Plus size={18} />
|
<Plus size={18} />
|
||||||
@ -906,112 +913,134 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
|||||||
>
|
>
|
||||||
<History size={18} />
|
<History size={18} />
|
||||||
</ChatHistory>
|
</ChatHistory>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
// switch between chat and prompts
|
||||||
|
if (tab === 'commands') {
|
||||||
|
setTab('chat')
|
||||||
|
} else {
|
||||||
|
setTab('commands')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="infio-chat-list-dropdown"
|
||||||
|
>
|
||||||
|
<SquareSlash size={18} color={tab === 'commands' ? 'var(--text-accent)' : 'var(--text-color)'} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="infio-chat-messages" ref={chatMessagesRef}>
|
{/* main view */}
|
||||||
{
|
{tab === 'chat' ? (
|
||||||
// If the chat is empty, show a message to start a new chat
|
<>
|
||||||
chatMessages.length === 0 && (
|
<div className="infio-chat-messages" ref={chatMessagesRef}>
|
||||||
<div className="infio-chat-empty-state">
|
{
|
||||||
<ShortcutInfo />
|
// If the chat is empty, show a message to start a new chat
|
||||||
</div>
|
chatMessages.length === 0 && (
|
||||||
)
|
<div className="infio-chat-empty-state">
|
||||||
}
|
<ShortcutInfo />
|
||||||
{chatMessages.map((message, index) =>
|
</div>
|
||||||
message.role === 'user' ? (
|
)
|
||||||
message.content &&
|
}
|
||||||
<div key={"user-" + message.id} className="infio-chat-messages-user">
|
{chatMessages.map((message, index) =>
|
||||||
<PromptInputWithActions
|
message.role === 'user' ? (
|
||||||
key={"input-" + message.id}
|
message.content &&
|
||||||
ref={(ref) => registerChatUserInputRef(message.id, ref)}
|
<div key={"user-" + message.id} className="infio-chat-messages-user">
|
||||||
initialSerializedEditorState={message.content}
|
<PromptInputWithActions
|
||||||
onSubmit={(content, useVaultSearch) => {
|
key={"input-" + message.id}
|
||||||
if (editorStateToPlainText(content).trim() === '') return
|
ref={(ref) => registerChatUserInputRef(message.id, ref)}
|
||||||
handleSubmit(
|
initialSerializedEditorState={message.content}
|
||||||
[
|
onSubmit={(content, useVaultSearch) => {
|
||||||
...chatMessages.slice(0, index),
|
if (editorStateToPlainText(content).trim() === '') return
|
||||||
{
|
handleSubmit(
|
||||||
role: 'user',
|
[
|
||||||
applyStatus: ApplyStatus.Idle,
|
...chatMessages.slice(0, index),
|
||||||
content: content,
|
{
|
||||||
promptContent: null,
|
role: 'user',
|
||||||
id: message.id,
|
applyStatus: ApplyStatus.Idle,
|
||||||
mentionables: message.mentionables,
|
content: content,
|
||||||
},
|
promptContent: null,
|
||||||
],
|
id: message.id,
|
||||||
useVaultSearch,
|
mentionables: message.mentionables,
|
||||||
)
|
},
|
||||||
chatUserInputRefs.current.get(inputMessage.id)?.focus()
|
],
|
||||||
}}
|
useVaultSearch,
|
||||||
onFocus={() => {
|
)
|
||||||
setFocusedMessageId(message.id)
|
chatUserInputRefs.current.get(inputMessage.id)?.focus()
|
||||||
}}
|
}}
|
||||||
mentionables={message.mentionables}
|
onFocus={() => {
|
||||||
setMentionables={(mentionables) => {
|
setFocusedMessageId(message.id)
|
||||||
setChatMessages((prevChatHistory) =>
|
}}
|
||||||
prevChatHistory.map((msg) =>
|
mentionables={message.mentionables}
|
||||||
msg.id === message.id ? { ...msg, mentionables } : msg,
|
setMentionables={(mentionables) => {
|
||||||
),
|
setChatMessages((prevChatHistory) =>
|
||||||
)
|
prevChatHistory.map((msg) =>
|
||||||
}}
|
msg.id === message.id ? { ...msg, mentionables } : msg,
|
||||||
/>
|
),
|
||||||
{message.similaritySearchResults && (
|
)
|
||||||
<SimilaritySearchResults
|
}}
|
||||||
key={"similarity-search-" + message.id}
|
/>
|
||||||
similaritySearchResults={message.similaritySearchResults}
|
{message.similaritySearchResults && (
|
||||||
/>
|
<SimilaritySearchResults
|
||||||
)}
|
key={"similarity-search-" + message.id}
|
||||||
</div>
|
similaritySearchResults={message.similaritySearchResults}
|
||||||
) : (
|
/>
|
||||||
<div key={"assistant-" + message.id} className="infio-chat-messages-assistant">
|
)}
|
||||||
<MarkdownReasoningBlock
|
</div>
|
||||||
key={"reasoning-" + message.id}
|
) : (
|
||||||
reasoningContent={message.reasoningContent} />
|
<div key={"assistant-" + message.id} className="infio-chat-messages-assistant">
|
||||||
<ReactMarkdownItem
|
<MarkdownReasoningBlock
|
||||||
key={"content-" + message.id}
|
key={"reasoning-" + message.id}
|
||||||
handleApply={(toolArgs) => handleApply(message.id, toolArgs)}
|
reasoningContent={message.reasoningContent} />
|
||||||
applyStatus={message.applyStatus}
|
<ReactMarkdownItem
|
||||||
>
|
key={"content-" + message.id}
|
||||||
{message.content}
|
handleApply={(toolArgs) => handleApply(message.id, toolArgs)}
|
||||||
</ReactMarkdownItem>
|
applyStatus={message.applyStatus}
|
||||||
</div>
|
>
|
||||||
),
|
{message.content}
|
||||||
)}
|
</ReactMarkdownItem>
|
||||||
<QueryProgress state={queryProgress} />
|
</div>
|
||||||
{submitMutation.isPending && (
|
),
|
||||||
<button onClick={abortActiveStreams} className="infio-stop-gen-btn">
|
)}
|
||||||
<CircleStop size={16} />
|
<QueryProgress state={queryProgress} />
|
||||||
<div>Stop generation</div>
|
{submitMutation.isPending && (
|
||||||
</button>
|
<button onClick={abortActiveStreams} className="infio-stop-gen-btn">
|
||||||
)}
|
<CircleStop size={16} />
|
||||||
</div>
|
<div>Stop generation</div>
|
||||||
<PromptInputWithActions
|
</button>
|
||||||
key={inputMessage.id}
|
)}
|
||||||
ref={(ref) => registerChatUserInputRef(inputMessage.id, ref)}
|
</div>
|
||||||
initialSerializedEditorState={inputMessage.content}
|
<PromptInputWithActions
|
||||||
onSubmit={(content, useVaultSearch) => {
|
key={inputMessage.id}
|
||||||
if (editorStateToPlainText(content).trim() === '') return
|
ref={(ref) => registerChatUserInputRef(inputMessage.id, ref)}
|
||||||
handleSubmit(
|
initialSerializedEditorState={inputMessage.content}
|
||||||
[...chatMessages, { ...inputMessage, content }],
|
onSubmit={(content, useVaultSearch) => {
|
||||||
useVaultSearch,
|
if (editorStateToPlainText(content).trim() === '') return
|
||||||
)
|
handleSubmit(
|
||||||
setInputMessage(getNewInputMessage(app, settings.defaultMention))
|
[...chatMessages, { ...inputMessage, content }],
|
||||||
preventAutoScrollRef.current = false
|
useVaultSearch,
|
||||||
handleScrollToBottom()
|
)
|
||||||
}}
|
setInputMessage(getNewInputMessage(app, settings.defaultMention))
|
||||||
onFocus={() => {
|
preventAutoScrollRef.current = false
|
||||||
setFocusedMessageId(inputMessage.id)
|
handleScrollToBottom()
|
||||||
}}
|
}}
|
||||||
mentionables={inputMessage.mentionables}
|
onFocus={() => {
|
||||||
setMentionables={(mentionables) => {
|
setFocusedMessageId(inputMessage.id)
|
||||||
setInputMessage((prevInputMessage) => ({
|
}}
|
||||||
...prevInputMessage,
|
mentionables={inputMessage.mentionables}
|
||||||
mentionables,
|
setMentionables={(mentionables) => {
|
||||||
}))
|
setInputMessage((prevInputMessage) => ({
|
||||||
}}
|
...prevInputMessage,
|
||||||
autoFocus
|
mentionables,
|
||||||
addedBlockKey={addedBlockKey}
|
}))
|
||||||
/>
|
}}
|
||||||
|
autoFocus
|
||||||
|
addedBlockKey={addedBlockKey}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="infio-chat-commands">
|
||||||
|
<CommandsView />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
243
src/components/chat-view/CommandsView.tsx
Normal file
243
src/components/chat-view/CommandsView.tsx
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
import { Pencil, Save, Search, Trash2 } from 'lucide-react'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
export interface Command {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommandsView = () => {
|
||||||
|
const [commands, setCommands] = useState<Command[]>([])
|
||||||
|
const [newCommand, setNewCommand] = useState<Command>({
|
||||||
|
id: uuidv4(),
|
||||||
|
title: '',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const [editingCommandId, setEditingCommandId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const titleInputRefs = useRef<Map<string, HTMLInputElement>>(new Map())
|
||||||
|
const contentInputRefs = useRef<Map<string, HTMLTextAreaElement>>(new Map())
|
||||||
|
|
||||||
|
// 从本地存储加载commands
|
||||||
|
useEffect(() => {
|
||||||
|
const savedCommands = localStorage.getItem('commands')
|
||||||
|
if (savedCommands) {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(savedCommands)
|
||||||
|
|
||||||
|
// 验证解析的数据是否为符合Prompt接口的数组
|
||||||
|
if (Array.isArray(parsedData) && parsedData.every(isCommand)) {
|
||||||
|
setCommands(parsedData)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('无法解析保存的命令', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 类型守卫函数,用于验证对象是否符合Command接口
|
||||||
|
function isCommand(item: unknown): item is Command {
|
||||||
|
if (!item || typeof item !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用in操作符检查属性存在
|
||||||
|
if (!('id' in item) || !('title' in item) || !('content' in item)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用JavaScript的hasOwnProperty和typeof来检查属性类型
|
||||||
|
return (
|
||||||
|
Object.prototype.hasOwnProperty.call(item, 'id') &&
|
||||||
|
Object.prototype.hasOwnProperty.call(item, 'title') &&
|
||||||
|
Object.prototype.hasOwnProperty.call(item, 'content') &&
|
||||||
|
typeof Reflect.get(item, 'id') === 'string' &&
|
||||||
|
typeof Reflect.get(item, 'title') === 'string' &&
|
||||||
|
typeof Reflect.get(item, 'content') === 'string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存commands到本地存储
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('commands', JSON.stringify(commands))
|
||||||
|
}, [commands])
|
||||||
|
|
||||||
|
// 处理新command的标题变化
|
||||||
|
const handleNewCommandTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setNewCommand({ ...newCommand, title: e.target.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理新command的内容变化
|
||||||
|
const handleNewCommandContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setNewCommand({ ...newCommand, content: e.target.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新command
|
||||||
|
const handleAddCommand = () => {
|
||||||
|
if (newCommand.title.trim() === '' || newCommand.content.trim() === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setCommands([...commands, newCommand])
|
||||||
|
setNewCommand({
|
||||||
|
id: uuidv4(),
|
||||||
|
title: '',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除command
|
||||||
|
const handleDeleteCommand = (id: string) => {
|
||||||
|
setCommands(commands.filter(command => command.id !== id))
|
||||||
|
if (editingCommandId === id) {
|
||||||
|
setEditingCommandId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑command
|
||||||
|
const handleEditCommand = (command: Command) => {
|
||||||
|
setEditingCommandId(command.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存编辑后的command
|
||||||
|
const handleSaveEdit = (id: string) => {
|
||||||
|
const titleInput = titleInputRefs.current.get(id)
|
||||||
|
const contentInput = contentInputRefs.current.get(id)
|
||||||
|
|
||||||
|
if (titleInput && contentInput) {
|
||||||
|
setCommands(
|
||||||
|
commands.map(command =>
|
||||||
|
command.id === id
|
||||||
|
? { ...command, title: titleInput.value, content: contentInput.value }
|
||||||
|
: command
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setEditingCommandId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearchTerm(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤commands列表
|
||||||
|
const filteredCommands = commands.filter(
|
||||||
|
command =>
|
||||||
|
command.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
command.content.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="infio-commands-container">
|
||||||
|
{/* header */}
|
||||||
|
<div className="infio-commands-header">
|
||||||
|
<div className="infio-commands-new">
|
||||||
|
<h2 className="infio-commands-header-title">Create Quick Command</h2>
|
||||||
|
<div className="infio-commands-label">Name</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Input Command Name"
|
||||||
|
value={newCommand.title}
|
||||||
|
onChange={handleNewCommandTitleChange}
|
||||||
|
className="infio-commands-input"
|
||||||
|
/>
|
||||||
|
<div className="infio-commands-label">Content</div>
|
||||||
|
<textarea
|
||||||
|
placeholder="Input Command Content"
|
||||||
|
value={newCommand.content}
|
||||||
|
onChange={handleNewCommandContentChange}
|
||||||
|
className="infio-commands-textarea"
|
||||||
|
/>
|
||||||
|
{/* <div className="infio-commands-hint">English identifier (lowercase letters + numbers + hyphens)</div> */}
|
||||||
|
<button
|
||||||
|
onClick={handleAddCommand}
|
||||||
|
className="infio-commands-add-btn"
|
||||||
|
disabled={!newCommand.title.trim() || !newCommand.content.trim()}
|
||||||
|
>
|
||||||
|
<span>Create Command</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* search bar */}
|
||||||
|
<div className="infio-commands-search">
|
||||||
|
<Search size={18} className="infio-commands-search-icon" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search Command..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={handleSearch}
|
||||||
|
className="infio-commands-search-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* commands list */}
|
||||||
|
<div className="infio-commands-list">
|
||||||
|
{filteredCommands.length === 0 ? (
|
||||||
|
<div className="infio-commands-empty">
|
||||||
|
<p>No commands found</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredCommands.map(command => (
|
||||||
|
<div key={command.id} className="infio-commands-item">
|
||||||
|
{editingCommandId === command.id ? (
|
||||||
|
// edit mode
|
||||||
|
<div className="infio-commands-edit-mode">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
defaultValue={command.title}
|
||||||
|
className="infio-commands-edit-title"
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) titleInputRefs.current.set(command.id, el)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
defaultValue={command.content}
|
||||||
|
className="infio-commands-textarea"
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) contentInputRefs.current.set(command.id, el)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="infio-commands-actions">
|
||||||
|
<button
|
||||||
|
onClick={() => handleSaveEdit(command.id)}
|
||||||
|
className="infio-commands-btn"
|
||||||
|
>
|
||||||
|
<Save size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// view mode
|
||||||
|
<div className="infio-commands-view-mode">
|
||||||
|
<div className="infio-commands-title">{command.title}</div>
|
||||||
|
<div className="infio-commands-content">{command.content}</div>
|
||||||
|
<div className="infio-commands-actions">
|
||||||
|
<button
|
||||||
|
onClick={() => handleEditCommand(command)}
|
||||||
|
className="infio-commands-btn"
|
||||||
|
>
|
||||||
|
<Pencil size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteCommand(command.id)}
|
||||||
|
className="infio-commands-btn"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandsView
|
||||||
1635
src/styles.css
1635
src/styles.css
File diff suppressed because it is too large
Load Diff
289
styles.css
289
styles.css
@ -1871,3 +1871,292 @@ button.infio-chat-input-model-select {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* CommandsView Styles
|
||||||
|
* - 命令管理界面
|
||||||
|
*/
|
||||||
|
|
||||||
|
.infio-chat-commands {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-container {
|
||||||
|
/* background-color: #1e1e1e; */
|
||||||
|
color: var(--text-normal);
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
padding: var(--size-4-3);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-header {
|
||||||
|
padding-bottom: var(--size-4-2);
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
margin-bottom: var(--size-4-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-new {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-4-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-header-title {
|
||||||
|
color: var(--text-normal);
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0 0 var(--size-4-3) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-label {
|
||||||
|
color: var(--text-normal);
|
||||||
|
font-size: var(--font-ui-medium);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
margin: var(--size-2-2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-hint {
|
||||||
|
color: #999;
|
||||||
|
font-size: var(--font-ui-smaller);
|
||||||
|
margin-top: var(--size-2-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-input {
|
||||||
|
background-color: #333 !important;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
color: var(--text-normal);
|
||||||
|
padding: var(--size-4-2);
|
||||||
|
font-size: var(--font-ui-small);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-bottom: var(--size-4-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-textarea {
|
||||||
|
background-color: #333 !important;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
color: var(--text-normal);
|
||||||
|
padding: var(--size-4-2);
|
||||||
|
font-size: var(--font-ui-small);
|
||||||
|
width: 100%;
|
||||||
|
min-height: 80px;
|
||||||
|
resize: vertical;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-add-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--size-2-2);
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-accent);
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
padding: var(--size-2-3) var(--size-4-3);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-ui-small);
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: var(--size-4-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-add-btn:disabled {
|
||||||
|
background-color: #444;
|
||||||
|
color: #777;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #333 !important;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin-bottom: var(--size-4-3);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
height: 36px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-search:focus-within {
|
||||||
|
border-color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-search-icon {
|
||||||
|
color: #888;
|
||||||
|
margin-right: 8px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-search-input {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
color: #ddd;
|
||||||
|
padding: 4px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
height: 24px;
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-search-input::placeholder {
|
||||||
|
color: #888;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-search-input::placeholder {
|
||||||
|
color: var(--text-faint);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-4-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-empty {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--size-4-3);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-item {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
padding: var(--size-4-2);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-title {
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
margin-bottom: var(--size-2-3);
|
||||||
|
font-size: var(--font-ui-medium);
|
||||||
|
color: var(--text-accent);
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-content {
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: var(--size-4-2);
|
||||||
|
font-size: var(--font-ui-small);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--size-1-2);
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 0 !important; /* 确保没有内边距 */
|
||||||
|
margin: 0 !important; /* 确保没有外边距 */
|
||||||
|
width: 24px !important; /* 限制宽度 */
|
||||||
|
height: 24px !important; /* 限制高度 */
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--background-modifier-hover) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-edit-mode {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-4-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-edit-title {
|
||||||
|
background-color: #333 !important;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
color: var(--text-normal);
|
||||||
|
padding: var(--size-4-2);
|
||||||
|
font-size: var(--font-ui-medium);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-view-mode {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-4-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-2-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-section {
|
||||||
|
margin-bottom: var(--size-4-3);
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
padding-bottom: var(--size-4-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-section-title {
|
||||||
|
color: var(--text-accent);
|
||||||
|
font-size: var(--font-ui-medium);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
margin: 0 0 var(--size-4-2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-location {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--size-4-4);
|
||||||
|
margin-bottom: var(--size-4-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-location-option {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
padding: var(--size-4-2);
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-location-option input[type="radio"] {
|
||||||
|
margin-right: var(--size-2-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-location-option label {
|
||||||
|
color: var(--text-normal);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
margin-bottom: var(--size-2-2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-commands-location-description {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--font-ui-smaller);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user