mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-05-06 06:56:29 +00:00
update workspace
This commit is contained in:
@@ -2,7 +2,7 @@ import * as path from 'path'
|
||||
|
||||
import { BaseSerializedNode } from '@lexical/clipboard/clipboard'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { CircleStop, History, NotebookPen, Plus, Search, Server, SquareSlash, Undo } from 'lucide-react'
|
||||
import { Box, CircleStop, History, NotebookPen, Plus, Search, Server, SquareSlash, Undo } from 'lucide-react'
|
||||
import { App, Notice, TFile, WorkspaceLeaf } from 'obsidian'
|
||||
import {
|
||||
forwardRef,
|
||||
@@ -18,6 +18,7 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { ApplyView, ApplyViewState } from '../../ApplyView'
|
||||
import { APPLY_VIEW_TYPE } from '../../constants'
|
||||
import { useApp } from '../../contexts/AppContext'
|
||||
import { useDataview } from '../../contexts/DataviewContext'
|
||||
import { useDiffStrategy } from '../../contexts/DiffStrategyContext'
|
||||
import { useLLM } from '../../contexts/LLMContext'
|
||||
import { useMcpHub } from '../../contexts/McpHubContext'
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
LLMBaseUrlNotSetException,
|
||||
LLMModelNotSetException,
|
||||
} from '../../core/llm/exception'
|
||||
import { TransformationType, runTransformation } from '../../core/transformations/run_trans'
|
||||
import { useChatHistory } from '../../hooks/use-chat-history'
|
||||
import { useCustomModes } from '../../hooks/use-custom-mode'
|
||||
import { t } from '../../lang/helpers'
|
||||
@@ -73,6 +75,8 @@ import SearchView from './SearchView'
|
||||
import SimilaritySearchResults from './SimilaritySearchResults'
|
||||
import UserMessageView from './UserMessageView'
|
||||
import WebsiteReadResults from './WebsiteReadResults'
|
||||
import WorkspaceSelect from './WorkspaceSelect'
|
||||
import WorkspaceView from './WorkspaceView'
|
||||
|
||||
// Add an empty line here
|
||||
const getNewInputMessage = (app: App, defaultMention: string): ChatUserMessage => {
|
||||
@@ -115,6 +119,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
const { settings, setSettings } = useSettings()
|
||||
const { getRAGEngine } = useRAG()
|
||||
const diffStrategy = useDiffStrategy()
|
||||
const dataviewManager = useDataview()
|
||||
const { getMcpHub } = useMcpHub()
|
||||
const { customModeList, customModePrompts } = useCustomModes()
|
||||
|
||||
@@ -179,7 +184,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
}
|
||||
}
|
||||
|
||||
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp' | 'search' | 'history'>('chat')
|
||||
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp' | 'search' | 'history' | 'workspace'>('chat')
|
||||
|
||||
const [selectedSerializedNodes, setSelectedSerializedNodes] = useState<BaseSerializedNode[]>([])
|
||||
|
||||
@@ -792,6 +797,124 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
mentionables: [],
|
||||
}
|
||||
}
|
||||
} else if (toolArgs.type === 'dataview_query') {
|
||||
if (!dataviewManager) {
|
||||
throw new Error('DataviewManager 未初始化')
|
||||
}
|
||||
|
||||
if (!dataviewManager.isDataviewAvailable()) {
|
||||
throw new Error('Dataview 插件未安装或未启用,请先安装并启用 Dataview 插件')
|
||||
}
|
||||
|
||||
// 执行 Dataview 查询
|
||||
const result = await dataviewManager.executeQuery(toolArgs.query)
|
||||
|
||||
let formattedContent: string;
|
||||
if (result.success) {
|
||||
formattedContent = `[dataview_query] 查询成功:\n${result.data}`;
|
||||
} else {
|
||||
formattedContent = `[dataview_query] 查询失败:\n${result.error}`;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'dataview_query',
|
||||
applyMsgId,
|
||||
applyStatus: result.success ? ApplyStatus.Applied : ApplyStatus.Failed,
|
||||
returnMsg: {
|
||||
role: 'user',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
content: null,
|
||||
promptContent: formattedContent,
|
||||
id: uuidv4(),
|
||||
mentionables: [],
|
||||
}
|
||||
}
|
||||
} else if (toolArgs.type === 'analyze_paper' ||
|
||||
toolArgs.type === 'key_insights' ||
|
||||
toolArgs.type === 'dense_summary' ||
|
||||
toolArgs.type === 'reflections' ||
|
||||
toolArgs.type === 'table_of_contents' ||
|
||||
toolArgs.type === 'simple_summary') {
|
||||
// 处理文档转换工具
|
||||
console.log('toolArgs', toolArgs)
|
||||
|
||||
try {
|
||||
// 获取文件
|
||||
const targetFile = app.vault.getFileByPath(toolArgs.path)
|
||||
if (!targetFile) {
|
||||
throw new Error(`文件未找到: ${toolArgs.path}`)
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
const fileContent = await readTFileContentPdf(targetFile, app.vault, app)
|
||||
|
||||
// 映射工具类型到转换类型
|
||||
const transformationTypeMap: Record<string, TransformationType> = {
|
||||
'analyze_paper': TransformationType.ANALYZE_PAPER,
|
||||
'key_insights': TransformationType.KEY_INSIGHTS,
|
||||
'dense_summary': TransformationType.DENSE_SUMMARY,
|
||||
'reflections': TransformationType.REFLECTIONS,
|
||||
'table_of_contents': TransformationType.TABLE_OF_CONTENTS,
|
||||
'simple_summary': TransformationType.SIMPLE_SUMMARY
|
||||
}
|
||||
|
||||
const transformationType = transformationTypeMap[toolArgs.type]
|
||||
if (!transformationType) {
|
||||
throw new Error(`不支持的转换类型: ${toolArgs.type}`)
|
||||
}
|
||||
|
||||
// 执行转换
|
||||
const transformationResult = await runTransformation({
|
||||
content: fileContent,
|
||||
transformationType,
|
||||
settings,
|
||||
model: {
|
||||
provider: settings.applyModelProvider,
|
||||
modelId: settings.applyModelId,
|
||||
}
|
||||
})
|
||||
|
||||
if (!transformationResult.success) {
|
||||
throw new Error(transformationResult.error || '转换失败')
|
||||
}
|
||||
|
||||
// 构建结果消息
|
||||
let formattedContent = `[${toolArgs.type}] 转换完成:\n\n${transformationResult.result}`
|
||||
|
||||
// 如果内容被截断,添加提示
|
||||
if (transformationResult.truncated) {
|
||||
formattedContent += `\n\n*注意: 原始内容过长(${transformationResult.originalTokens} tokens),已截断为${transformationResult.processedTokens} tokens进行处理*`
|
||||
}
|
||||
|
||||
return {
|
||||
type: toolArgs.type,
|
||||
applyMsgId,
|
||||
applyStatus: ApplyStatus.Applied,
|
||||
returnMsg: {
|
||||
role: 'user',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
content: null,
|
||||
promptContent: formattedContent,
|
||||
id: uuidv4(),
|
||||
mentionables: [],
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`转换失败 (${toolArgs.type}):`, error)
|
||||
return {
|
||||
type: toolArgs.type,
|
||||
applyMsgId,
|
||||
applyStatus: ApplyStatus.Failed,
|
||||
returnMsg: {
|
||||
role: 'user',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
content: null,
|
||||
promptContent: `[${toolArgs.type}] 转换失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
id: uuidv4(),
|
||||
mentionables: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to apply changes', error)
|
||||
@@ -1006,7 +1129,9 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
<div className="infio-chat-container">
|
||||
{/* header view */}
|
||||
<div className="infio-chat-header">
|
||||
INFIO
|
||||
<div className="infio-chat-header-title">
|
||||
{t('workspace.shortTitle')}: <WorkspaceSelect />
|
||||
</div>
|
||||
<div className="infio-chat-header-buttons">
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -1029,6 +1154,18 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
>
|
||||
<History size={18} color={tab === 'history' ? 'var(--text-accent)' : 'var(--text-color)'} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (tab === 'workspace') {
|
||||
setTab('chat')
|
||||
} else {
|
||||
setTab('workspace')
|
||||
}
|
||||
}}
|
||||
className="infio-chat-list-dropdown"
|
||||
>
|
||||
<Box size={18} color={tab === 'workspace' ? 'var(--text-accent)' : 'var(--text-color)'} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (tab === 'search') {
|
||||
@@ -1275,6 +1412,10 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : tab === 'workspace' ? (
|
||||
<div className="infio-chat-commands">
|
||||
<WorkspaceView />
|
||||
</div>
|
||||
) : (
|
||||
<div className="infio-chat-commands">
|
||||
<McpHubView />
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Check, ChevronDown, ChevronRight, Database, Loader2, X } from 'lucide-react'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { t } from '../../../lang/helpers'
|
||||
import { ApplyStatus, DataviewQueryToolArgs } from "../../../types/apply"
|
||||
|
||||
export default function MarkdownDataviewQueryBlock({
|
||||
applyStatus,
|
||||
onApply,
|
||||
query,
|
||||
outputFormat,
|
||||
finish
|
||||
}: {
|
||||
applyStatus: ApplyStatus
|
||||
onApply: (args: DataviewQueryToolArgs) => void
|
||||
query: string
|
||||
outputFormat: string
|
||||
finish: boolean
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (finish && applyStatus === ApplyStatus.Idle) {
|
||||
onApply({
|
||||
type: 'dataview_query',
|
||||
query: query,
|
||||
outputFormat: outputFormat,
|
||||
})
|
||||
}
|
||||
}, [finish])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`infio-chat-code-block has-filename`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div className={'infio-chat-code-block-header'}>
|
||||
<div
|
||||
className={'infio-chat-code-block-header-filename'}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
style={{ cursor: isHovered ? 'pointer' : 'default' }}
|
||||
>
|
||||
{isHovered ? (
|
||||
isOpen ? <ChevronDown size={14} className="infio-chat-code-block-header-icon" /> : <ChevronRight size={14} className="infio-chat-code-block-header-icon" />
|
||||
) : (
|
||||
<Database size={14} className="infio-chat-code-block-header-icon" />
|
||||
)}
|
||||
Dataview 查询 ({outputFormat})
|
||||
</div>
|
||||
<div className={'infio-chat-code-block-header-button'}>
|
||||
<button
|
||||
className="infio-dataview-query-button"
|
||||
disabled={true}
|
||||
>
|
||||
{
|
||||
!finish || applyStatus === ApplyStatus.Idle ? (
|
||||
<>
|
||||
<Loader2 className="spinner" size={14} /> 执行中...
|
||||
</>
|
||||
) : applyStatus === ApplyStatus.Applied ? (
|
||||
<>
|
||||
<Check size={14} /> 完成
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X size={14} /> 失败
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className={'infio-chat-code-block-content'}>
|
||||
<pre>
|
||||
<code>{query}</code>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChevronDown, ChevronRight, Brain } from 'lucide-react'
|
||||
import { Brain, ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { PropsWithChildren, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { useDarkModeContext } from "../../../contexts/DarkModeContext"
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
import { useApp } from "../../../contexts/AppContext"
|
||||
import { ApplyStatus, ToolArgs } from "../../../types/apply"
|
||||
import { openMarkdownFile } from "../../../utils/obsidian"
|
||||
|
||||
export type TransformationToolType = 'analyze_paper' | 'key_insights' | 'dense_summary' | 'reflections' | 'table_of_contents' | 'simple_summary'
|
||||
|
||||
interface MarkdownTransformationToolBlockProps {
|
||||
applyStatus: ApplyStatus
|
||||
onApply: (args: ToolArgs) => void
|
||||
toolType: TransformationToolType
|
||||
path: string
|
||||
depth?: number
|
||||
format?: string
|
||||
include_summary?: boolean
|
||||
finish: boolean
|
||||
}
|
||||
|
||||
const getToolConfig = (toolType: TransformationToolType) => {
|
||||
switch (toolType) {
|
||||
case 'analyze_paper':
|
||||
return {
|
||||
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
|
||||
title: 'Analyze Paper',
|
||||
description: 'Deep analysis of academic papers'
|
||||
}
|
||||
case 'key_insights':
|
||||
return {
|
||||
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
|
||||
title: 'Key Insights',
|
||||
description: 'Extract key insights'
|
||||
}
|
||||
case 'dense_summary':
|
||||
return {
|
||||
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
|
||||
title: 'Dense Summary',
|
||||
description: 'Create information-dense summary'
|
||||
}
|
||||
case 'reflections':
|
||||
return {
|
||||
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
|
||||
title: 'Deep Reflections',
|
||||
description: 'Generate deep reflections'
|
||||
}
|
||||
case 'table_of_contents':
|
||||
return {
|
||||
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
|
||||
title: 'Table of Contents',
|
||||
description: 'Generate table of contents structure'
|
||||
}
|
||||
case 'simple_summary':
|
||||
return {
|
||||
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
|
||||
title: 'Simple Summary',
|
||||
description: 'Create readable summary'
|
||||
}
|
||||
default:
|
||||
return {
|
||||
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
|
||||
title: 'Document Processing',
|
||||
description: 'Process document'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function MarkdownTransformationToolBlock({
|
||||
applyStatus,
|
||||
onApply,
|
||||
toolType,
|
||||
path,
|
||||
depth,
|
||||
format,
|
||||
include_summary,
|
||||
finish
|
||||
}: MarkdownTransformationToolBlockProps) {
|
||||
const app = useApp()
|
||||
const config = getToolConfig(toolType)
|
||||
|
||||
const handleClick = () => {
|
||||
if (path) {
|
||||
openMarkdownFile(app, path)
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (finish && applyStatus === ApplyStatus.Idle) {
|
||||
// 构建符合标准ToolArgs类型的参数
|
||||
if (toolType === 'table_of_contents') {
|
||||
onApply({
|
||||
type: toolType,
|
||||
path: path || '',
|
||||
depth,
|
||||
format,
|
||||
include_summary
|
||||
})
|
||||
} else {
|
||||
onApply({
|
||||
type: toolType,
|
||||
path: path || '',
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [finish])
|
||||
|
||||
const getDisplayText = () => {
|
||||
if (toolType === 'table_of_contents') {
|
||||
let text = `${config.title}: ${path || '未指定路径'}`
|
||||
if (depth) text += ` (深度: ${depth})`
|
||||
if (format) text += ` (格式: ${format})`
|
||||
if (include_summary) text += ` (包含摘要)`
|
||||
return text
|
||||
}
|
||||
return `${config.title}: ${path || '未指定路径'}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`infio-chat-code-block ${path ? 'has-filename' : ''}`}
|
||||
onClick={handleClick}
|
||||
style={{ cursor: path ? 'pointer' : 'default' }}
|
||||
>
|
||||
<div className={'infio-chat-code-block-header'}>
|
||||
<div className={'infio-chat-code-block-header-filename'}>
|
||||
{config.icon}
|
||||
<span>{getDisplayText()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '../../utils/parse-infio-block'
|
||||
|
||||
import MarkdownApplyDiffBlock from './Markdown/MarkdownApplyDiffBlock'
|
||||
import MarkdownDataviewQueryBlock from './Markdown/MarkdownDataviewQueryBlock'
|
||||
import MarkdownEditFileBlock from './Markdown/MarkdownEditFileBlock'
|
||||
import MarkdownFetchUrlsContentBlock from './Markdown/MarkdownFetchUrlsContentBlock'
|
||||
import MarkdownListFilesBlock from './Markdown/MarkdownListFilesBlock'
|
||||
@@ -19,6 +20,7 @@ import MarkdownSearchWebBlock from './Markdown/MarkdownSearchWebBlock'
|
||||
import MarkdownSemanticSearchFilesBlock from './Markdown/MarkdownSemanticSearchFilesBlock'
|
||||
import MarkdownSwitchModeBlock from './Markdown/MarkdownSwitchModeBlock'
|
||||
import MarkdownToolResult from './Markdown/MarkdownToolResult'
|
||||
import MarkdownTransformationToolBlock from './Markdown/MarkdownTransformationToolBlock'
|
||||
import MarkdownWithIcons from './Markdown/MarkdownWithIcon'
|
||||
import RawMarkdownBlock from './Markdown/RawMarkdownBlock'
|
||||
import UseMcpToolBlock from './Markdown/UseMcpToolBlock'
|
||||
@@ -202,6 +204,72 @@ function ReactMarkdown({
|
||||
parameters={block.parameters}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'dataview_query' ? (
|
||||
<MarkdownDataviewQueryBlock
|
||||
key={"dataview-query-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
query={block.query}
|
||||
outputFormat={block.outputFormat}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'analyze_paper' ? (
|
||||
<MarkdownTransformationToolBlock
|
||||
key={"analyze-paper-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
toolType="analyze_paper"
|
||||
path={block.path}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'key_insights' ? (
|
||||
<MarkdownTransformationToolBlock
|
||||
key={"key-insights-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
toolType="key_insights"
|
||||
path={block.path}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'dense_summary' ? (
|
||||
<MarkdownTransformationToolBlock
|
||||
key={"dense-summary-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
toolType="dense_summary"
|
||||
path={block.path}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'reflections' ? (
|
||||
<MarkdownTransformationToolBlock
|
||||
key={"reflections-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
toolType="reflections"
|
||||
path={block.path}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'table_of_contents' ? (
|
||||
<MarkdownTransformationToolBlock
|
||||
key={"table-of-contents-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
toolType="table_of_contents"
|
||||
path={block.path}
|
||||
depth={block.depth}
|
||||
format={block.format}
|
||||
include_summary={block.include_summary}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'simple_summary' ? (
|
||||
<MarkdownTransformationToolBlock
|
||||
key={"simple-summary-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
toolType="simple_summary"
|
||||
path={block.path}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'tool_result' ? (
|
||||
<MarkdownToolResult
|
||||
key={"tool-result-" + index}
|
||||
|
||||
893
src/components/chat-view/WorkspaceEditModal.tsx
Normal file
893
src/components/chat-view/WorkspaceEditModal.tsx
Normal file
@@ -0,0 +1,893 @@
|
||||
import { ChevronDown, FolderOpen, Plus, Tag, Trash2, X } from 'lucide-react'
|
||||
import { App, TFile, TFolder } from 'obsidian'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { Workspace, WorkspaceContent } from '../../database/json/workspace/types'
|
||||
import { t } from '../../lang/helpers'
|
||||
import { createDataviewManager } from '../../utils/dataview'
|
||||
|
||||
interface WorkspaceEditModalProps {
|
||||
workspace?: Workspace
|
||||
app: App
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSave: (updatedWorkspace: Partial<Workspace>) => Promise<void>
|
||||
}
|
||||
|
||||
const WorkspaceEditModal = ({
|
||||
workspace,
|
||||
app,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave
|
||||
}: WorkspaceEditModalProps) => {
|
||||
// 生成默认工作区名称
|
||||
const getDefaultWorkspaceName = () => {
|
||||
const now = new Date()
|
||||
const date = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`
|
||||
return t('workspace.editModal.defaultName', { date })
|
||||
}
|
||||
|
||||
const [name, setName] = useState(workspace?.name || getDefaultWorkspaceName())
|
||||
const [content, setContent] = useState<WorkspaceContent[]>(workspace?.content ? [...workspace.content] : [])
|
||||
const [availableFolders, setAvailableFolders] = useState<string[]>([])
|
||||
const [availableTags, setAvailableTags] = useState<string[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// 智能添加相关状态
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [showSuggestions, setShowSuggestions] = useState(false)
|
||||
const [filteredSuggestions, setFilteredSuggestions] = useState<{type: 'folder' | 'tag', value: string}[]>([])
|
||||
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const suggestionsRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 获取可用的文件夹和标签
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
|
||||
const loadAvailableOptions = async () => {
|
||||
// 获取所有文件夹
|
||||
const folders: string[] = []
|
||||
const addFolder = (folder: TFolder) => {
|
||||
folders.push(folder.path)
|
||||
// folder.children.forEach(child => {
|
||||
// if (child instanceof TFolder) {
|
||||
// addFolder(child)
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
app.vault.getAllFolders(false).forEach(folder => {
|
||||
addFolder(folder)
|
||||
})
|
||||
|
||||
setAvailableFolders(folders.sort())
|
||||
|
||||
// 使用 dataview 查询获取所有标签
|
||||
const dataviewManager = createDataviewManager(app)
|
||||
|
||||
if (dataviewManager.isDataviewAvailable()) {
|
||||
try {
|
||||
const result = await dataviewManager.executeQuery('TABLE file.tags FROM ""')
|
||||
|
||||
if (result.success && result.data) {
|
||||
const tags = new Set<string>()
|
||||
|
||||
// 解析结果中的标签
|
||||
const lines = result.data.split('\n')
|
||||
lines.forEach(line => {
|
||||
if (line.includes('#')) {
|
||||
const tagMatches = line.match(/#[a-zA-Z0-9\u4e00-\u9fa5_-]+/g)
|
||||
if (tagMatches) {
|
||||
tagMatches.forEach(tag => tags.add(tag))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setAvailableTags(Array.from(tags).sort())
|
||||
} else {
|
||||
// 回退到传统方法
|
||||
fallbackToTraditionalTagQuery()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Dataview 查询失败:', error)
|
||||
// 回退到传统方法
|
||||
fallbackToTraditionalTagQuery()
|
||||
}
|
||||
} else {
|
||||
// 回退到传统方法
|
||||
fallbackToTraditionalTagQuery()
|
||||
}
|
||||
}
|
||||
|
||||
// 传统方法获取标签(作为回退方案)
|
||||
const fallbackToTraditionalTagQuery = () => {
|
||||
const tags = new Set<string>()
|
||||
app.vault.getAllLoadedFiles().forEach(file => {
|
||||
if (file instanceof TFile) {
|
||||
const cache = app.metadataCache.getFileCache(file)
|
||||
if (cache?.tags) {
|
||||
cache.tags.forEach(tag => {
|
||||
tags.add(tag.tag)
|
||||
})
|
||||
}
|
||||
if (cache?.frontmatter?.tags) {
|
||||
const frontmatterTags = cache.frontmatter.tags
|
||||
if (Array.isArray(frontmatterTags)) {
|
||||
frontmatterTags.forEach(tag => tags.add(`#${tag}`))
|
||||
} else if (typeof frontmatterTags === 'string') {
|
||||
tags.add(`#${frontmatterTags}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setAvailableTags(Array.from(tags).sort())
|
||||
}
|
||||
|
||||
loadAvailableOptions()
|
||||
}, [isOpen, app])
|
||||
|
||||
// 重置表单
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setName(workspace?.name || getDefaultWorkspaceName())
|
||||
setContent(workspace?.content ? [...workspace.content] : [])
|
||||
}
|
||||
}, [isOpen, workspace])
|
||||
|
||||
// 更新建议列表
|
||||
useEffect(() => {
|
||||
if (!inputValue.trim()) {
|
||||
setFilteredSuggestions([])
|
||||
setShowSuggestions(false)
|
||||
return
|
||||
}
|
||||
|
||||
const suggestions: {type: 'folder' | 'tag', value: string}[] = []
|
||||
const searchTerm = inputValue.toLowerCase()
|
||||
|
||||
// 搜索匹配的文件夹
|
||||
availableFolders.forEach(folder => {
|
||||
if (folder.toLowerCase().includes(searchTerm)) {
|
||||
// 检查是否已存在
|
||||
const exists = content.some(item =>
|
||||
item.type === 'folder' && item.content === folder
|
||||
)
|
||||
if (!exists) {
|
||||
suggestions.push({ type: 'folder', value: folder })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 搜索匹配的标签
|
||||
availableTags.forEach(tag => {
|
||||
if (tag.toLowerCase().includes(searchTerm)) {
|
||||
// 检查是否已存在
|
||||
const exists = content.some(item =>
|
||||
item.type === 'tag' && item.content === tag
|
||||
)
|
||||
if (!exists) {
|
||||
suggestions.push({ type: 'tag', value: tag })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 如果输入以#开头,优先显示标签建议
|
||||
if (inputValue.startsWith('#')) {
|
||||
suggestions.sort((a, b) => {
|
||||
if (a.type === 'tag' && b.type !== 'tag') return -1
|
||||
if (a.type !== 'tag' && b.type === 'tag') return 1
|
||||
return 0
|
||||
})
|
||||
} else {
|
||||
// 否则优先显示文件夹建议
|
||||
suggestions.sort((a, b) => {
|
||||
if (a.type === 'folder' && b.type !== 'folder') return -1
|
||||
if (a.type !== 'folder' && b.type === 'folder') return 1
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
setFilteredSuggestions(suggestions.slice(0, 10)) // 限制显示数量
|
||||
setShowSuggestions(suggestions.length > 0)
|
||||
setSelectedSuggestionIndex(-1)
|
||||
}, [inputValue, availableFolders, availableTags, content])
|
||||
|
||||
// 添加内容项
|
||||
const addContentItem = (type: 'folder' | 'tag', contentValue: string) => {
|
||||
if (!contentValue.trim()) return
|
||||
|
||||
// 检查是否已存在
|
||||
const exists = content.some(item =>
|
||||
item.type === type && item.content === contentValue
|
||||
)
|
||||
|
||||
if (exists) return
|
||||
|
||||
const newItem: WorkspaceContent = {
|
||||
type,
|
||||
content: contentValue
|
||||
}
|
||||
|
||||
setContent([...content, newItem])
|
||||
|
||||
// 清空输入框和建议
|
||||
setInputValue('')
|
||||
setShowSuggestions(false)
|
||||
setSelectedSuggestionIndex(-1)
|
||||
}
|
||||
|
||||
// 处理建议选择
|
||||
const handleSuggestionSelect = (suggestion: {type: 'folder' | 'tag', value: string}) => {
|
||||
addContentItem(suggestion.type, suggestion.value)
|
||||
}
|
||||
|
||||
// 处理手动添加
|
||||
const handleManualAdd = () => {
|
||||
const value = inputValue.trim()
|
||||
if (!value) return
|
||||
|
||||
// 自动判断类型
|
||||
const type = value.startsWith('#') ? 'tag' : 'folder'
|
||||
addContentItem(type, value)
|
||||
}
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (!showSuggestions) {
|
||||
if (e.key === 'Enter') {
|
||||
handleManualAdd()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
setSelectedSuggestionIndex(prev =>
|
||||
prev < filteredSuggestions.length - 1 ? prev + 1 : prev
|
||||
)
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
setSelectedSuggestionIndex(prev => prev > 0 ? prev - 1 : -1)
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
if (selectedSuggestionIndex >= 0 && selectedSuggestionIndex < filteredSuggestions.length) {
|
||||
handleSuggestionSelect(filteredSuggestions[selectedSuggestionIndex])
|
||||
} else {
|
||||
handleManualAdd()
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
setShowSuggestions(false)
|
||||
setSelectedSuggestionIndex(-1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部关闭建议
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target
|
||||
if (
|
||||
target instanceof Node &&
|
||||
inputRef.current &&
|
||||
!inputRef.current.contains(target) &&
|
||||
suggestionsRef.current &&
|
||||
!suggestionsRef.current.contains(target)
|
||||
) {
|
||||
setShowSuggestions(false)
|
||||
setSelectedSuggestionIndex(-1)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
// 删除内容项
|
||||
const removeContentItem = (index: number) => {
|
||||
setContent(content.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
// 保存更改
|
||||
const handleSave = async () => {
|
||||
if (!name.trim()) {
|
||||
alert(t('workspace.editModal.nameRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
await onSave({
|
||||
name: name.trim(),
|
||||
content
|
||||
})
|
||||
onClose()
|
||||
} catch (error) {
|
||||
console.error('保存工作区失败:', error)
|
||||
alert(t('workspace.editModal.saveFailed'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="workspace-edit-modal-overlay">
|
||||
<div className="workspace-edit-modal">
|
||||
{/* 头部 */}
|
||||
<div className="workspace-edit-modal-header">
|
||||
<h3>{workspace ? t('workspace.editModal.editTitle') : t('workspace.editModal.createTitle')}</h3>
|
||||
<button
|
||||
className="workspace-edit-modal-close"
|
||||
onClick={onClose}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
<div className="workspace-edit-modal-content">
|
||||
{/* 工作区名称 */}
|
||||
<div className="workspace-edit-section">
|
||||
<label className="workspace-edit-label">{t('workspace.editModal.nameLabel')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="workspace-edit-input"
|
||||
placeholder={workspace ? t('workspace.editModal.namePlaceholder') : t('workspace.editModal.newNamePlaceholder')}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 工作区内容 */}
|
||||
<div className="workspace-edit-section">
|
||||
<label className="workspace-edit-label">{t('workspace.editModal.contentLabel')}</label>
|
||||
|
||||
{/* 当前内容列表 */}
|
||||
<div className="workspace-content-list">
|
||||
{content.map((item, index) => (
|
||||
<div key={index} className="workspace-content-item">
|
||||
<div className="workspace-content-item-info">
|
||||
{item.type === 'folder' ? (
|
||||
<FolderOpen size={16} />
|
||||
) : (
|
||||
<Tag size={16} />
|
||||
)}
|
||||
<span className="workspace-content-item-text">
|
||||
{item.content}
|
||||
</span>
|
||||
<span className="workspace-content-item-type">
|
||||
({item.type === 'folder' ? t('workspace.editModal.folder') : t('workspace.editModal.tag')})
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="workspace-content-item-remove"
|
||||
onClick={() => removeContentItem(index)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{content.length === 0 && (
|
||||
<div className="workspace-content-empty">
|
||||
{t('workspace.editModal.noContent')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 智能添加 - 作为内容列表的一部分 */}
|
||||
<div className="workspace-smart-add-item">
|
||||
<div className="workspace-smart-add-container">
|
||||
<div className={`workspace-smart-input-wrapper ${showSuggestions ? 'has-suggestions' : ''}`}>
|
||||
<Plus size={16} className="workspace-smart-add-icon" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => {
|
||||
if (filteredSuggestions.length > 0) {
|
||||
setShowSuggestions(true)
|
||||
}
|
||||
}}
|
||||
placeholder={t('workspace.editModal.addPlaceholder')}
|
||||
className="workspace-smart-input"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{showSuggestions && (
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className="workspace-smart-dropdown-icon workspace-smart-dropdown-icon-up"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 建议下拉列表 */}
|
||||
{showSuggestions && filteredSuggestions.length > 0 && (
|
||||
<div ref={suggestionsRef} className="workspace-suggestions">
|
||||
{filteredSuggestions.map((suggestion, index) => (
|
||||
<div
|
||||
key={`${suggestion.type}-${suggestion.value}`}
|
||||
className={`workspace-suggestion-item ${
|
||||
index === selectedSuggestionIndex ? 'selected' : ''
|
||||
}`}
|
||||
onClick={() => handleSuggestionSelect(suggestion)}
|
||||
onMouseEnter={() => setSelectedSuggestionIndex(index)}
|
||||
>
|
||||
<div className="workspace-suggestion-content">
|
||||
{suggestion.type === 'folder' ? (
|
||||
<FolderOpen size={14} />
|
||||
) : (
|
||||
<Tag size={14} />
|
||||
)}
|
||||
<span className="workspace-suggestion-text">
|
||||
{suggestion.value}
|
||||
</span>
|
||||
<span className="workspace-suggestion-type">
|
||||
{suggestion.type === 'folder' ? t('workspace.editModal.folder') : t('workspace.editModal.tag')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="workspace-smart-add-tip">
|
||||
{t('workspace.editModal.tip')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<div className="workspace-edit-modal-footer">
|
||||
<button
|
||||
className="workspace-edit-btn workspace-edit-btn-cancel"
|
||||
onClick={onClose}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t('workspace.editModal.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="workspace-edit-btn workspace-edit-btn-save"
|
||||
onClick={handleSave}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading
|
||||
? (workspace ? t('workspace.editModal.saving') : t('workspace.editModal.creating'))
|
||||
: (workspace ? t('workspace.editModal.save') : t('workspace.editModal.create'))
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 样式 */}
|
||||
<style>
|
||||
{`
|
||||
.workspace-edit-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.workspace-edit-modal {
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.workspace-edit-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.workspace-edit-modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workspace-edit-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.workspace-edit-modal-close:hover:not(:disabled) {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.workspace-edit-modal-close:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.workspace-edit-modal-content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.workspace-edit-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.workspace-edit-label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.workspace-edit-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.workspace-edit-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.workspace-edit-input:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.workspace-content-list {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
margin-bottom: 2px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.workspace-content-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.workspace-content-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.workspace-content-list::-webkit-scrollbar-thumb {
|
||||
background-color: var(--background-modifier-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.workspace-content-list::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
.workspace-content-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.workspace-content-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.workspace-content-item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.workspace-content-item-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.workspace-content-item-type {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.workspace-content-item-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--text-error);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.workspace-content-item-remove:hover:not(:disabled) {
|
||||
background-color: var(--background-modifier-error);
|
||||
}
|
||||
|
||||
.workspace-content-item-remove:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.workspace-content-empty {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.workspace-smart-add-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.workspace-smart-add-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.workspace-smart-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--background-primary);
|
||||
padding: 8px 12px;
|
||||
gap: 8px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.workspace-smart-input-wrapper:hover {
|
||||
border-color: var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
.workspace-smart-input-wrapper:focus-within {
|
||||
border-color: var(--text-accent);
|
||||
box-shadow: 0 0 0 2px rgba(var(--text-accent-rgb), 0.1);
|
||||
}
|
||||
|
||||
.workspace-smart-input-wrapper.has-suggestions {
|
||||
border-radius: 0 0 var(--radius-s) var(--radius-s);
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
.workspace-smart-input-wrapper.has-suggestions:focus-within {
|
||||
border-radius: 0 0 var(--radius-s) var(--radius-s);
|
||||
border-top-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.workspace-smart-add-container:focus-within .workspace-suggestions {
|
||||
border-color: var(--text-accent);
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(var(--text-accent-rgb), 0.1);
|
||||
}
|
||||
|
||||
.workspace-smart-input {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-normal);
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.workspace-smart-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.workspace-smart-input:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.workspace-smart-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.workspace-smart-add-icon {
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.workspace-smart-dropdown-icon {
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.workspace-smart-dropdown-icon-up {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.workspace-suggestions {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-bottom: none;
|
||||
border-radius: var(--radius-s) var(--radius-s) 0 0;
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.workspace-suggestions::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.workspace-suggestions::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.workspace-suggestions::-webkit-scrollbar-thumb {
|
||||
background-color: var(--background-modifier-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.workspace-suggestions::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
.workspace-suggestion-item {
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.workspace-suggestion-item:first-child {
|
||||
border-radius: var(--radius-s) var(--radius-s) 0 0;
|
||||
}
|
||||
|
||||
.workspace-suggestion-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.workspace-suggestion-item:hover,
|
||||
.workspace-suggestion-item.selected {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.workspace-suggestion-item.selected {
|
||||
background-color: var(--background-modifier-active-hover);
|
||||
}
|
||||
|
||||
.workspace-suggestion-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.workspace-suggestion-text {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
color: var(--text-normal);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workspace-suggestion-type {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
background-color: var(--background-secondary);
|
||||
padding: 2px 6px;
|
||||
border-radius: calc(var(--radius-s) - 1px);
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.workspace-suggestion-item:hover .workspace-suggestion-type,
|
||||
.workspace-suggestion-item.selected .workspace-suggestion-type {
|
||||
background-color: var(--background-modifier-border);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.workspace-smart-add-tip {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
margin-top: 8px;
|
||||
padding: 0 2px;
|
||||
background-color: var(--background-secondary-alt);
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--radius-s);
|
||||
border-left: 2px solid var(--text-accent);
|
||||
}
|
||||
|
||||
.workspace-edit-modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.workspace-edit-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-s);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.workspace-edit-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.workspace-edit-btn-cancel {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.workspace-edit-btn-cancel:hover:not(:disabled) {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.workspace-edit-btn-save {
|
||||
background-color: var(--text-accent);
|
||||
border: 1px solid var(--text-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.workspace-edit-btn-save:hover:not(:disabled) {
|
||||
background-color: var(--text-accent-hover);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkspaceEditModal
|
||||
295
src/components/chat-view/WorkspaceSelect.tsx
Normal file
295
src/components/chat-view/WorkspaceSelect.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { Check, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { Notice } from 'obsidian'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { useApp } from '../../contexts/AppContext'
|
||||
import { useSettings } from '../../contexts/SettingsContext'
|
||||
import { Workspace } from '../../database/json/workspace/types'
|
||||
import { WorkspaceManager } from '../../database/json/workspace/WorkspaceManager'
|
||||
|
||||
interface WorkspaceInfo extends Workspace {
|
||||
isCurrent: boolean
|
||||
}
|
||||
|
||||
const WorkspaceSelect = () => {
|
||||
const app = useApp()
|
||||
const { settings, setSettings } = useSettings()
|
||||
const [workspaces, setWorkspaces] = useState<WorkspaceInfo[]>([])
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [workspaceManager, setWorkspaceManager] = useState<WorkspaceManager | null>(null)
|
||||
|
||||
// 初始化工作区管理器
|
||||
useEffect(() => {
|
||||
const manager = new WorkspaceManager(app)
|
||||
setWorkspaceManager(manager)
|
||||
}, [app])
|
||||
|
||||
// 获取当前工作区名称
|
||||
const getCurrentWorkspaceName = () => {
|
||||
return settings.workspace || 'vault'
|
||||
}
|
||||
|
||||
// 获取工作区列表
|
||||
const getWorkspaces = useCallback(async () => {
|
||||
if (!workspaceManager) return []
|
||||
|
||||
try {
|
||||
// 确保默认 vault 工作区存在
|
||||
await workspaceManager.ensureDefaultVaultWorkspace()
|
||||
|
||||
// 获取所有工作区
|
||||
const workspaceMetadata = await workspaceManager.listWorkspaces()
|
||||
|
||||
const workspaceList: WorkspaceInfo[] = []
|
||||
const currentWorkspaceName = getCurrentWorkspaceName()
|
||||
|
||||
for (const meta of workspaceMetadata) {
|
||||
const workspace = await workspaceManager.findById(meta.id)
|
||||
if (workspace) {
|
||||
workspaceList.push({
|
||||
...workspace,
|
||||
isCurrent: workspace.name === currentWorkspaceName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 按名称排序,vault 排在最前面
|
||||
workspaceList.sort((a, b) => {
|
||||
if (a.name === 'vault') return -1
|
||||
if (b.name === 'vault') return 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
|
||||
return workspaceList
|
||||
} catch (error) {
|
||||
console.error('获取工作区列表失败:', error)
|
||||
return []
|
||||
}
|
||||
}, [workspaceManager, settings.workspace])
|
||||
|
||||
// 刷新工作区列表
|
||||
const refreshWorkspaces = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const workspaceList = await getWorkspaces()
|
||||
setWorkspaces(workspaceList)
|
||||
} catch (error) {
|
||||
console.error('刷新工作区列表失败:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [getWorkspaces])
|
||||
|
||||
// 切换到指定工作区
|
||||
const switchToWorkspace = async (workspace: WorkspaceInfo) => {
|
||||
if (workspace.isCurrent) {
|
||||
setIsOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新设置中的工作区
|
||||
setSettings({
|
||||
...settings,
|
||||
workspace: workspace.name
|
||||
})
|
||||
|
||||
// 关闭下拉菜单
|
||||
setIsOpen(false)
|
||||
|
||||
// 刷新工作区列表以更新状态
|
||||
await refreshWorkspaces()
|
||||
} catch (error) {
|
||||
console.error('切换工作区失败:', error)
|
||||
new Notice('切换工作区失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化和设置变化时刷新
|
||||
useEffect(() => {
|
||||
refreshWorkspaces()
|
||||
}, [refreshWorkspaces])
|
||||
|
||||
// 下拉菜单打开时刷新数据
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (open && !isOpen) {
|
||||
refreshWorkspaces()
|
||||
}
|
||||
setIsOpen(open)
|
||||
}
|
||||
|
||||
const currentWorkspace = workspaces.find(w => w.isCurrent)
|
||||
const displayName = currentWorkspace?.name || getCurrentWorkspaceName()
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu.Root open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenu.Trigger className="infio-workspace-select">
|
||||
<span className="infio-workspace-select__name">
|
||||
{displayName}
|
||||
</span>
|
||||
<div className="infio-workspace-select__icon">
|
||||
{isOpen ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
||||
</div>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content className="infio-popover infio-workspace-select-content">
|
||||
{isLoading ? (
|
||||
<div className="infio-workspace-loading">
|
||||
加载中...
|
||||
</div>
|
||||
) : workspaces.length === 0 ? (
|
||||
<div className="infio-workspace-empty">
|
||||
暂无工作区
|
||||
</div>
|
||||
) : (
|
||||
<ul>
|
||||
{workspaces.map((workspace) => (
|
||||
<DropdownMenu.Item
|
||||
key={workspace.id}
|
||||
onSelect={() => switchToWorkspace(workspace)}
|
||||
asChild
|
||||
>
|
||||
<li className={`infio-workspace-item`}>
|
||||
<span className="infio-workspace-item-name">
|
||||
{workspace.name}
|
||||
</span>
|
||||
{workspace.isCurrent && (
|
||||
<Check size={14} className="infio-workspace-check" />
|
||||
)}
|
||||
</li>
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<style>{`
|
||||
button.infio-workspace-select {
|
||||
background-color: var(--background-modifier-hover);
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
padding: var(--size-4-1) var(--size-4-1);
|
||||
font-size: var(--font-small);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
gap: var(--size-2-2);
|
||||
border-radius: var(--radius-l);
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
button.infio-workspace-select:hover {
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
button.infio-workspace-select:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.infio-workspace-select__name {
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.infio-workspace-select__icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.infio-workspace-select-content {
|
||||
min-width: auto !important;
|
||||
width: fit-content !important;
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.infio-workspace-loading,
|
||||
.infio-workspace-empty {
|
||||
padding: var(--size-4-3) var(--size-4-2);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-small);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.infio-workspace-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: var(--size-4-2) var(--size-4-2);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.infio-workspace-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-2-1);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.infio-workspace-item-name {
|
||||
font-size: var(--font-small);
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.infio-workspace-item-info {
|
||||
font-size: var(--font-smallest);
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.infio-workspace-check {
|
||||
color: var(--text-accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.infio-workspace-select-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.infio-workspace-select-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.infio-workspace-select-content::-webkit-scrollbar-thumb {
|
||||
background: var(--background-modifier-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.infio-workspace-select-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--background-modifier-border-hover);
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkspaceSelect
|
||||
907
src/components/chat-view/WorkspaceView.tsx
Normal file
907
src/components/chat-view/WorkspaceView.tsx
Normal file
@@ -0,0 +1,907 @@
|
||||
import {
|
||||
ArrowRight,
|
||||
Box,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
FolderOpen,
|
||||
MessageSquare,
|
||||
Pencil,
|
||||
Plus,
|
||||
RotateCcw,
|
||||
Tag,
|
||||
Trash2
|
||||
} from 'lucide-react'
|
||||
import { Notice } from 'obsidian'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { useApp } from '../../contexts/AppContext'
|
||||
import { useSettings } from '../../contexts/SettingsContext'
|
||||
import { Workspace, WorkspaceContent } from '../../database/json/workspace/types'
|
||||
import { WorkspaceManager } from '../../database/json/workspace/WorkspaceManager'
|
||||
import { t } from '../../lang/helpers'
|
||||
|
||||
import WorkspaceEditModal from './WorkspaceEditModal'
|
||||
|
||||
interface WorkspaceInfo extends Workspace {
|
||||
isCurrent: boolean
|
||||
}
|
||||
|
||||
const WorkspaceView = () => {
|
||||
const app = useApp()
|
||||
const { settings, setSettings } = useSettings()
|
||||
const [workspaces, setWorkspaces] = useState<WorkspaceInfo[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [workspaceManager, setWorkspaceManager] = useState<WorkspaceManager | null>(null)
|
||||
const [editingWorkspace, setEditingWorkspace] = useState<Workspace | null>(null)
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
|
||||
|
||||
// 初始化工作区管理器
|
||||
useEffect(() => {
|
||||
const manager = new WorkspaceManager(app)
|
||||
setWorkspaceManager(manager)
|
||||
}, [app])
|
||||
|
||||
// 获取当前工作区名称
|
||||
const getCurrentWorkspaceName = (): string => {
|
||||
return settings.workspace || 'vault'
|
||||
}
|
||||
|
||||
// 获取工作区列表
|
||||
const getWorkspaces = useCallback(async (): Promise<WorkspaceInfo[]> => {
|
||||
if (!workspaceManager) return []
|
||||
|
||||
try {
|
||||
// 确保默认 vault 工作区存在
|
||||
await workspaceManager.ensureDefaultVaultWorkspace()
|
||||
|
||||
// 获取所有工作区
|
||||
const workspaceMetadata = await workspaceManager.listWorkspaces()
|
||||
|
||||
const workspaceList: WorkspaceInfo[] = []
|
||||
const currentWorkspaceName = getCurrentWorkspaceName()
|
||||
|
||||
for (const meta of workspaceMetadata) {
|
||||
const workspace = await workspaceManager.findById(meta.id)
|
||||
if (workspace) {
|
||||
workspaceList.push({
|
||||
...workspace,
|
||||
isCurrent: workspace.name === currentWorkspaceName
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return workspaceList
|
||||
} catch (error) {
|
||||
console.error('获取工作区列表失败:', error)
|
||||
return []
|
||||
}
|
||||
}, [workspaceManager, settings.workspace])
|
||||
|
||||
// 刷新工作区列表
|
||||
const refreshWorkspaces = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const workspaceList = await getWorkspaces()
|
||||
setWorkspaces(workspaceList)
|
||||
} catch (error) {
|
||||
console.error('刷新工作区列表失败:', error)
|
||||
new Notice(String(t('workspace.notices.refreshFailed')))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [getWorkspaces])
|
||||
|
||||
// 切换到指定工作区
|
||||
const switchToWorkspace = async (workspace: WorkspaceInfo) => {
|
||||
if (workspace.isCurrent) {
|
||||
new Notice(String(t('workspace.notices.alreadyInWorkspace')))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新设置中的工作区
|
||||
setSettings({
|
||||
...settings,
|
||||
workspace: workspace.name
|
||||
})
|
||||
|
||||
// 刷新工作区列表以更新状态
|
||||
await refreshWorkspaces()
|
||||
} catch (error) {
|
||||
console.error('切换工作区失败:', error)
|
||||
new Notice(String(t('workspace.notices.switchFailed')))
|
||||
}
|
||||
}
|
||||
|
||||
// 删除工作区
|
||||
const deleteWorkspace = async (workspace: WorkspaceInfo) => {
|
||||
if (!workspaceManager) return
|
||||
|
||||
if (workspace.isCurrent) {
|
||||
new Notice(String(t('workspace.notices.cannotDeleteCurrent')))
|
||||
return
|
||||
}
|
||||
|
||||
if (workspace.name === 'vault') {
|
||||
new Notice(String(t('workspace.notices.cannotDeleteDefault')))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await workspaceManager.deleteWorkspace(workspace.id)
|
||||
if (success) {
|
||||
new Notice(String(t('workspace.notices.deleted', { name: workspace.name })))
|
||||
await refreshWorkspaces()
|
||||
} else {
|
||||
new Notice(String(t('workspace.notices.deleteFailed')))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除工作区失败:', error)
|
||||
new Notice(String(t('workspace.notices.deleteFailed')))
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新工作区
|
||||
const createNewWorkspace = () => {
|
||||
setIsCreateModalOpen(true)
|
||||
}
|
||||
|
||||
// 关闭创建模态框
|
||||
const closeCreateModal = () => {
|
||||
setIsCreateModalOpen(false)
|
||||
}
|
||||
|
||||
// 保存新工作区
|
||||
const saveNewWorkspace = async (workspaceData: Partial<Workspace>) => {
|
||||
if (!workspaceManager) return
|
||||
|
||||
try {
|
||||
const newWorkspace = await workspaceManager.createWorkspace({
|
||||
name: workspaceData.name || String(t('workspace.newWorkspace')),
|
||||
content: workspaceData.content || [],
|
||||
metadata: {
|
||||
description: workspaceData.metadata?.description || String(t('workspace.newWorkspace'))
|
||||
}
|
||||
})
|
||||
|
||||
new Notice(String(t('workspace.notices.created', { name: newWorkspace.name })))
|
||||
await refreshWorkspaces()
|
||||
closeCreateModal()
|
||||
} catch (error) {
|
||||
console.error('创建工作区失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 打开编辑工作区模态框
|
||||
const openEditModal = (workspace: WorkspaceInfo) => {
|
||||
setEditingWorkspace(workspace)
|
||||
setIsEditModalOpen(true)
|
||||
}
|
||||
|
||||
// 关闭编辑模态框
|
||||
const closeEditModal = () => {
|
||||
setIsEditModalOpen(false)
|
||||
setEditingWorkspace(null)
|
||||
}
|
||||
|
||||
// 保存工作区编辑
|
||||
const saveWorkspaceEdit = async (updates: Partial<Workspace>) => {
|
||||
if (!workspaceManager || !editingWorkspace) return
|
||||
|
||||
try {
|
||||
await workspaceManager.updateWorkspace(editingWorkspace.id, updates)
|
||||
new Notice(String(t('workspace.notices.updated', { name: updates.name || editingWorkspace.name })))
|
||||
await refreshWorkspaces()
|
||||
} catch (error) {
|
||||
console.error('更新工作区失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化工作区内容
|
||||
const formatWorkspaceContent = (content: WorkspaceContent[]): string => {
|
||||
if (content.length === 0) return String(t('workspace.empty'))
|
||||
|
||||
const folders = content.filter(c => c.type === 'folder').length
|
||||
const tags = content.filter(c => c.type === 'tag').length
|
||||
|
||||
const parts = []
|
||||
if (folders > 0) parts.push(`${folders} ${String(t('workspace.folders'))}`)
|
||||
if (tags > 0) parts.push(`${tags} ${String(t('workspace.tags'))}`)
|
||||
|
||||
return parts.join(', ') || String(t('workspace.noContent'))
|
||||
}
|
||||
|
||||
// 展开状态管理
|
||||
const [expandedWorkspaces, setExpandedWorkspaces] = useState<Set<string>>(new Set())
|
||||
const [expandedChats, setExpandedChats] = useState<Set<string>>(new Set())
|
||||
|
||||
// 切换工作区内容展开状态
|
||||
const toggleWorkspaceExpanded = (workspaceId: string) => {
|
||||
const newExpanded = new Set(expandedWorkspaces)
|
||||
if (newExpanded.has(workspaceId)) {
|
||||
newExpanded.delete(workspaceId)
|
||||
} else {
|
||||
newExpanded.add(workspaceId)
|
||||
}
|
||||
setExpandedWorkspaces(newExpanded)
|
||||
}
|
||||
|
||||
// 切换对话历史展开状态
|
||||
const toggleChatExpanded = (workspaceId: string) => {
|
||||
const newExpanded = new Set(expandedChats)
|
||||
if (newExpanded.has(workspaceId)) {
|
||||
newExpanded.delete(workspaceId)
|
||||
} else {
|
||||
newExpanded.add(workspaceId)
|
||||
}
|
||||
setExpandedChats(newExpanded)
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatLastOpened = (timestamp?: number) => {
|
||||
if (!timestamp) return '未知'
|
||||
|
||||
const now = Date.now()
|
||||
const diff = now - timestamp
|
||||
const minutes = Math.floor(diff / 60000)
|
||||
const hours = Math.floor(diff / 3600000)
|
||||
const days = Math.floor(diff / 86400000)
|
||||
|
||||
if (minutes < 1) return '刚刚'
|
||||
if (minutes < 60) return `${minutes} 分钟前`
|
||||
if (hours < 24) return `${hours} 小时前`
|
||||
if (days < 7) return `${days} 天前`
|
||||
|
||||
return new Date(timestamp).toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// 组件初始化
|
||||
useEffect(() => {
|
||||
refreshWorkspaces().catch((error) => {
|
||||
console.error('初始化工作区列表失败:', error)
|
||||
})
|
||||
}, [refreshWorkspaces])
|
||||
|
||||
return (
|
||||
<div className="infio-workspace-view-container">
|
||||
{/* 头部 */}
|
||||
<div className="infio-workspace-view-header">
|
||||
<div className="infio-workspace-view-title">
|
||||
<h2>{t('workspace.title')}</h2>
|
||||
</div>
|
||||
<div className="infio-workspace-view-header-actions">
|
||||
<button
|
||||
onClick={refreshWorkspaces}
|
||||
className="infio-workspace-view-refresh-btn"
|
||||
disabled={isLoading}
|
||||
title={t('workspace.refreshTooltip')}
|
||||
>
|
||||
<RotateCcw size={16} className={isLoading ? 'spinning' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 描述 */}
|
||||
<div className="infio-workspace-view-tip">
|
||||
{t('workspace.description')}
|
||||
</div>
|
||||
|
||||
{/* 创建新工作区按钮 */}
|
||||
<div className="infio-workspace-view-create-action">
|
||||
<button
|
||||
className="infio-workspace-view-create-btn"
|
||||
onClick={createNewWorkspace}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Plus size={16} />
|
||||
<span>{t('workspace.createNew')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 工作区列表 */}
|
||||
<div className="infio-workspace-view-list">
|
||||
<div className="infio-workspace-view-list-header">
|
||||
<h3>{t('workspace.recentWorkspaces')}</h3>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="infio-workspace-view-loading">
|
||||
{t('workspace.loading')}
|
||||
</div>
|
||||
) : workspaces.length === 0 ? (
|
||||
<div className="infio-workspace-view-empty">
|
||||
<Box size={48} className="infio-workspace-view-empty-icon" />
|
||||
<p>{t('workspace.noWorkspaces')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="infio-workspace-view-items">
|
||||
{workspaces.map((workspace, index) => (
|
||||
<div
|
||||
key={workspace.id || index}
|
||||
className={`infio-workspace-view-item ${workspace.isCurrent ? 'current' : ''}`}
|
||||
>
|
||||
<div className="infio-workspace-view-item-header">
|
||||
<div className="infio-workspace-view-item-icon">
|
||||
{workspace.isCurrent ? (
|
||||
<Box size={20} />
|
||||
) : (
|
||||
<Box size={20} />
|
||||
)}
|
||||
</div>
|
||||
<div className="infio-workspace-view-item-name">
|
||||
{workspace.name}
|
||||
{workspace.isCurrent && (
|
||||
<span className="infio-workspace-view-current-badge">{String(t('workspace.current'))}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="infio-workspace-view-item-actions">
|
||||
{!workspace.isCurrent && (
|
||||
<button
|
||||
onClick={() => switchToWorkspace(workspace)}
|
||||
className="infio-workspace-view-action-btn switch-btn"
|
||||
title="切换到此工作区"
|
||||
>
|
||||
<ArrowRight size={16} />
|
||||
</button>
|
||||
)}
|
||||
{workspace.name !== 'vault' && (
|
||||
<button
|
||||
onClick={() => openEditModal(workspace)}
|
||||
className="infio-workspace-view-action-btn"
|
||||
title={String(t('workspace.editTooltip'))}
|
||||
>
|
||||
<Pencil size={16} />
|
||||
</button>
|
||||
)}
|
||||
{!workspace.isCurrent && workspace.name !== 'vault' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm(String(t('workspace.deleteConfirm', { name: workspace.name })))) {
|
||||
deleteWorkspace(workspace)
|
||||
}
|
||||
}}
|
||||
className="infio-workspace-view-action-btn danger"
|
||||
title={String(t('workspace.deleteTooltip'))}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="infio-workspace-view-item-content">
|
||||
{/* 工作区内容 */}
|
||||
<div
|
||||
className="infio-workspace-view-item-path clickable"
|
||||
onClick={() => toggleWorkspaceExpanded(workspace.id)}
|
||||
>
|
||||
<div className="infio-workspace-view-item-path-info">
|
||||
<FolderOpen size={12} />
|
||||
{formatWorkspaceContent(workspace.content)}
|
||||
</div>
|
||||
{workspace.content.length > 0 && (
|
||||
<div className="infio-workspace-view-expand-icon">
|
||||
{expandedWorkspaces.has(workspace.id) ? (
|
||||
<ChevronDown size={14} />
|
||||
) : (
|
||||
<ChevronRight size={14} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 展开的内容详情 */}
|
||||
{expandedWorkspaces.has(workspace.id) && workspace.content.length > 0 && (
|
||||
<div className="infio-workspace-view-content-details">
|
||||
<div className="infio-workspace-view-content-list">
|
||||
{workspace.content.map((item, itemIndex) => (
|
||||
<div key={itemIndex} className="infio-workspace-view-content-item">
|
||||
{item.type === 'folder' ? (
|
||||
<FolderOpen size={14} />
|
||||
) : (
|
||||
<Tag size={14} />
|
||||
)}
|
||||
<span className="infio-workspace-view-content-text">
|
||||
{item.content}
|
||||
</span>
|
||||
<span className="infio-workspace-view-content-type">
|
||||
{item.type === 'folder' ? '文件夹' : '标签'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 对话历史 */}
|
||||
<div
|
||||
className="infio-workspace-view-chat-info clickable"
|
||||
onClick={() => toggleChatExpanded(workspace.id)}
|
||||
>
|
||||
<div className="infio-workspace-view-chat-info-content">
|
||||
<MessageSquare size={12} />
|
||||
<span>{workspace.chatHistory.length} {String(t('workspace.conversations'))}</span>
|
||||
</div>
|
||||
{workspace.chatHistory.length > 0 && (
|
||||
<div className="infio-workspace-view-expand-icon">
|
||||
{expandedChats.has(workspace.id) ? (
|
||||
<ChevronDown size={14} />
|
||||
) : (
|
||||
<ChevronRight size={14} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 展开的对话历史详情 */}
|
||||
{expandedChats.has(workspace.id) && workspace.chatHistory.length > 0 && (
|
||||
<div className="infio-workspace-view-chat-details">
|
||||
<div className="infio-workspace-view-chat-list">
|
||||
{workspace.chatHistory.slice(-5).reverse().map((chat, chatIndex) => (
|
||||
<div key={chatIndex} className="infio-workspace-view-chat-item">
|
||||
<MessageSquare size={14} />
|
||||
<span className="infio-workspace-view-chat-title">
|
||||
{chat.title || `对话 ${chat.id.slice(0, 8)}`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="infio-workspace-view-item-meta">
|
||||
{String(t('workspace.created'))}: {new Date(workspace.createdAt).toLocaleDateString('zh-CN')} |
|
||||
{String(t('workspace.updated'))}: {formatLastOpened(workspace.updatedAt)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 编辑模态框 */}
|
||||
{editingWorkspace && (
|
||||
<WorkspaceEditModal
|
||||
workspace={editingWorkspace}
|
||||
app={app}
|
||||
isOpen={isEditModalOpen}
|
||||
onClose={closeEditModal}
|
||||
onSave={saveWorkspaceEdit}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 创建工作区模态框 */}
|
||||
<WorkspaceEditModal
|
||||
workspace={undefined}
|
||||
app={app}
|
||||
isOpen={isCreateModalOpen}
|
||||
onClose={closeCreateModal}
|
||||
onSave={saveNewWorkspace}
|
||||
/>
|
||||
|
||||
{/* 样式 */}
|
||||
<style>
|
||||
{`
|
||||
.infio-workspace-view-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
color: var(--text-normal);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.infio-workspace-view-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.infio-workspace-view-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-title h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.infio-workspace-view-header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infio-workspace-view-refresh-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;
|
||||
}
|
||||
}
|
||||
|
||||
.spinning {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.infio-workspace-view-tip {
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-create-action {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-create-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
color: var(--text-normal);
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-m);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.infio-workspace-view-create-btn:hover:not(:disabled) {
|
||||
background-color: var(--background-modifier-hover);
|
||||
border-color: var(--text-accent);
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.infio-workspace-view-create-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.infio-workspace-view-current {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
padding: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-current-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--text-normal);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-current-info {
|
||||
margin-left: 26px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-current-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-current-status {
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.infio-workspace-view-list-header h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.infio-workspace-view-loading {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.infio-workspace-view-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.infio-workspace-view-empty-icon {
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.infio-workspace-view-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
padding: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.infio-workspace-view-item.current {
|
||||
border-color: var(--background-modifier-border);
|
||||
background-color: var(--background-primary);
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-icon {
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
margin-bottom: 4px;
|
||||
justify-content: flex-start;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item.current .infio-workspace-view-item-icon {
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-current-badge {
|
||||
background-color: var(--text-accent);
|
||||
color: var(--text-on-accent);
|
||||
font-size: 12px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-path {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
margin-bottom: 2px;
|
||||
padding: 4px 0;
|
||||
border-radius: var(--radius-s);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-path.clickable {
|
||||
cursor: pointer;
|
||||
padding: 6px 8px;
|
||||
margin: -2px -4px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-path.clickable:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-path-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.infio-workspace-view-expand-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.infio-workspace-view-content-details {
|
||||
margin-top: 8px;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.infio-workspace-view-content-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-content-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: var(--radius-s);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-content-text {
|
||||
flex: 1;
|
||||
color: var(--text-normal);
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.infio-workspace-view-content-type {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
background-color: var(--background-modifier-border);
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
padding: 4px 0;
|
||||
border-radius: var(--radius-s);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-info.clickable {
|
||||
cursor: pointer;
|
||||
padding: 6px 8px;
|
||||
margin: 2px -4px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-info.clickable:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-info-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-details {
|
||||
margin-top: 8px;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: var(--radius-s);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-title {
|
||||
flex: 1;
|
||||
color: var(--text-normal);
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-meta {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-action-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;
|
||||
border-radius: var(--radius-s);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-modifier-hover) !important;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkspaceView
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { ChevronDown, ChevronUp, MessageSquare, SquarePen, Search } from 'lucide-react'
|
||||
import { ChevronDown, ChevronUp, MessageSquare, Search, SquarePen } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { useSettings } from '../../../contexts/SettingsContext'
|
||||
@@ -114,10 +114,10 @@ export function ModeSelect() {
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
height: var(--size-4-4);
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
gap: var(--size-2-2);
|
||||
border-radius: var(--radius-m);
|
||||
border-radius: var(--radius-l);
|
||||
transition: all 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
@@ -128,6 +128,7 @@ export function ModeSelect() {
|
||||
.infio-chat-input-mode-select__mode-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
margin-top: var(--size-4-1);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-accent);
|
||||
|
||||
Reference in New Issue
Block a user