add command view
This commit is contained in:
parent
4a5823721e
commit
43599fca47
@ -1,7 +1,7 @@
|
||||
import * as path from 'path'
|
||||
|
||||
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 {
|
||||
forwardRef,
|
||||
@ -52,6 +52,7 @@ import { ModeSelect } from './chat-input/ModeSelect'
|
||||
import PromptInputWithActions, { ChatUserInputRef } from './chat-input/PromptInputWithActions'
|
||||
import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text'
|
||||
import { ChatHistory } from './ChatHistory'
|
||||
import CommandsView from './CommandsView'
|
||||
import MarkdownReasoningBlock from './Markdown/MarkdownReasoningBlock'
|
||||
import QueryProgress, { QueryProgressState } from './QueryProgress'
|
||||
import ReactMarkdown from './ReactMarkdown'
|
||||
@ -160,6 +161,8 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
}
|
||||
}
|
||||
|
||||
const [tab, setTab] = useState<'chat' | 'commands'>('commands')
|
||||
|
||||
useEffect(() => {
|
||||
const scrollContainer = chatMessagesRef.current
|
||||
if (!scrollContainer) return
|
||||
@ -870,11 +873,15 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
|
||||
return (
|
||||
<div className="infio-chat-container">
|
||||
{/* header view */}
|
||||
<div className="infio-chat-header">
|
||||
<ModeSelect />
|
||||
<div className="infio-chat-header-buttons">
|
||||
<button
|
||||
onClick={() => handleNewChat()}
|
||||
onClick={() => {
|
||||
setTab('chat')
|
||||
handleNewChat()
|
||||
}}
|
||||
className="infio-chat-list-dropdown"
|
||||
>
|
||||
<Plus size={18} />
|
||||
@ -906,112 +913,134 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
>
|
||||
<History size={18} />
|
||||
</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 className="infio-chat-messages" ref={chatMessagesRef}>
|
||||
{
|
||||
// If the chat is empty, show a message to start a new chat
|
||||
chatMessages.length === 0 && (
|
||||
<div className="infio-chat-empty-state">
|
||||
<ShortcutInfo />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{chatMessages.map((message, index) =>
|
||||
message.role === 'user' ? (
|
||||
message.content &&
|
||||
<div key={"user-" + message.id} className="infio-chat-messages-user">
|
||||
<PromptInputWithActions
|
||||
key={"input-" + message.id}
|
||||
ref={(ref) => registerChatUserInputRef(message.id, ref)}
|
||||
initialSerializedEditorState={message.content}
|
||||
onSubmit={(content, useVaultSearch) => {
|
||||
if (editorStateToPlainText(content).trim() === '') return
|
||||
handleSubmit(
|
||||
[
|
||||
...chatMessages.slice(0, index),
|
||||
{
|
||||
role: 'user',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
content: content,
|
||||
promptContent: null,
|
||||
id: message.id,
|
||||
mentionables: message.mentionables,
|
||||
},
|
||||
],
|
||||
useVaultSearch,
|
||||
)
|
||||
chatUserInputRefs.current.get(inputMessage.id)?.focus()
|
||||
}}
|
||||
onFocus={() => {
|
||||
setFocusedMessageId(message.id)
|
||||
}}
|
||||
mentionables={message.mentionables}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div key={"assistant-" + message.id} className="infio-chat-messages-assistant">
|
||||
<MarkdownReasoningBlock
|
||||
key={"reasoning-" + message.id}
|
||||
reasoningContent={message.reasoningContent} />
|
||||
<ReactMarkdownItem
|
||||
key={"content-" + message.id}
|
||||
handleApply={(toolArgs) => handleApply(message.id, toolArgs)}
|
||||
applyStatus={message.applyStatus}
|
||||
>
|
||||
{message.content}
|
||||
</ReactMarkdownItem>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
<QueryProgress state={queryProgress} />
|
||||
{submitMutation.isPending && (
|
||||
<button onClick={abortActiveStreams} className="infio-stop-gen-btn">
|
||||
<CircleStop size={16} />
|
||||
<div>Stop generation</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<PromptInputWithActions
|
||||
key={inputMessage.id}
|
||||
ref={(ref) => registerChatUserInputRef(inputMessage.id, ref)}
|
||||
initialSerializedEditorState={inputMessage.content}
|
||||
onSubmit={(content, useVaultSearch) => {
|
||||
if (editorStateToPlainText(content).trim() === '') return
|
||||
handleSubmit(
|
||||
[...chatMessages, { ...inputMessage, content }],
|
||||
useVaultSearch,
|
||||
)
|
||||
setInputMessage(getNewInputMessage(app, settings.defaultMention))
|
||||
preventAutoScrollRef.current = false
|
||||
handleScrollToBottom()
|
||||
}}
|
||||
onFocus={() => {
|
||||
setFocusedMessageId(inputMessage.id)
|
||||
}}
|
||||
mentionables={inputMessage.mentionables}
|
||||
setMentionables={(mentionables) => {
|
||||
setInputMessage((prevInputMessage) => ({
|
||||
...prevInputMessage,
|
||||
mentionables,
|
||||
}))
|
||||
}}
|
||||
autoFocus
|
||||
addedBlockKey={addedBlockKey}
|
||||
/>
|
||||
{/* main view */}
|
||||
{tab === 'chat' ? (
|
||||
<>
|
||||
<div className="infio-chat-messages" ref={chatMessagesRef}>
|
||||
{
|
||||
// If the chat is empty, show a message to start a new chat
|
||||
chatMessages.length === 0 && (
|
||||
<div className="infio-chat-empty-state">
|
||||
<ShortcutInfo />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{chatMessages.map((message, index) =>
|
||||
message.role === 'user' ? (
|
||||
message.content &&
|
||||
<div key={"user-" + message.id} className="infio-chat-messages-user">
|
||||
<PromptInputWithActions
|
||||
key={"input-" + message.id}
|
||||
ref={(ref) => registerChatUserInputRef(message.id, ref)}
|
||||
initialSerializedEditorState={message.content}
|
||||
onSubmit={(content, useVaultSearch) => {
|
||||
if (editorStateToPlainText(content).trim() === '') return
|
||||
handleSubmit(
|
||||
[
|
||||
...chatMessages.slice(0, index),
|
||||
{
|
||||
role: 'user',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
content: content,
|
||||
promptContent: null,
|
||||
id: message.id,
|
||||
mentionables: message.mentionables,
|
||||
},
|
||||
],
|
||||
useVaultSearch,
|
||||
)
|
||||
chatUserInputRefs.current.get(inputMessage.id)?.focus()
|
||||
}}
|
||||
onFocus={() => {
|
||||
setFocusedMessageId(message.id)
|
||||
}}
|
||||
mentionables={message.mentionables}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div key={"assistant-" + message.id} className="infio-chat-messages-assistant">
|
||||
<MarkdownReasoningBlock
|
||||
key={"reasoning-" + message.id}
|
||||
reasoningContent={message.reasoningContent} />
|
||||
<ReactMarkdownItem
|
||||
key={"content-" + message.id}
|
||||
handleApply={(toolArgs) => handleApply(message.id, toolArgs)}
|
||||
applyStatus={message.applyStatus}
|
||||
>
|
||||
{message.content}
|
||||
</ReactMarkdownItem>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
<QueryProgress state={queryProgress} />
|
||||
{submitMutation.isPending && (
|
||||
<button onClick={abortActiveStreams} className="infio-stop-gen-btn">
|
||||
<CircleStop size={16} />
|
||||
<div>Stop generation</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<PromptInputWithActions
|
||||
key={inputMessage.id}
|
||||
ref={(ref) => registerChatUserInputRef(inputMessage.id, ref)}
|
||||
initialSerializedEditorState={inputMessage.content}
|
||||
onSubmit={(content, useVaultSearch) => {
|
||||
if (editorStateToPlainText(content).trim() === '') return
|
||||
handleSubmit(
|
||||
[...chatMessages, { ...inputMessage, content }],
|
||||
useVaultSearch,
|
||||
)
|
||||
setInputMessage(getNewInputMessage(app, settings.defaultMention))
|
||||
preventAutoScrollRef.current = false
|
||||
handleScrollToBottom()
|
||||
}}
|
||||
onFocus={() => {
|
||||
setFocusedMessageId(inputMessage.id)
|
||||
}}
|
||||
mentionables={inputMessage.mentionables}
|
||||
setMentionables={(mentionables) => {
|
||||
setInputMessage((prevInputMessage) => ({
|
||||
...prevInputMessage,
|
||||
mentionables,
|
||||
}))
|
||||
}}
|
||||
autoFocus
|
||||
addedBlockKey={addedBlockKey}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="infio-chat-commands">
|
||||
<CommandsView />
|
||||
</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;
|
||||
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