add chat view & edit line local lang

This commit is contained in:
duanfuxiang 2025-05-01 15:07:35 +08:00
parent dc4ce4aeca
commit 2f824134b6
28 changed files with 412 additions and 154 deletions

View File

@ -2,6 +2,7 @@ import * as Tooltip from '@radix-ui/react-tooltip'
import { Check, CopyIcon } from 'lucide-react'
import { useEffect, useState } from 'react'
import { t } from '../../lang/helpers'
import { ChatAssistantMessage } from '../../types/chat'
import { calculateLLMCost } from '../../utils/price-calculator'
@ -35,7 +36,7 @@ function CopyButton({ message }: { message: ChatAssistantMessage }) {
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="infio-tooltip-content">
Copy message
{t('chat.reactMarkdown.copyMsg')}
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
@ -76,7 +77,7 @@ function LLMResponesInfoButton({ message }: { message: ChatAssistantMessage }) {
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="infio-tooltip-content">
View details
{t('chat.reactMarkdown.viewDetails')}
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>

View File

@ -2,8 +2,10 @@ import * as Popover from '@radix-ui/react-popover'
import { Pencil, Trash2 } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { t } from '../../lang/helpers'
import { ChatConversationMeta } from '../../types/chat'
function TitleInput({
title,
onSubmit,
@ -165,7 +167,7 @@ export function ChatHistory({
<ul>
{chatList.length === 0 ? (
<li className="infio-chat-list-dropdown-empty">
No conversations
{t('chat.history.noConversations')}
</li>
) : (
chatList.map((chat, index) => (

View File

@ -31,6 +31,7 @@ import {
import { regexSearchFiles } from '../../core/ripgrep'
import { useChatHistory } from '../../hooks/use-chat-history'
import { useCustomModes } from '../../hooks/use-custom-mode'
import { t } from '../../lang/helpers'
import { ApplyStatus, ToolArgs } from '../../types/apply'
import { ChatMessage, ChatUserMessage } from '../../types/chat'
import {
@ -165,7 +166,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
}
}
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode'>('custom-mode')
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode'>('chat')
const [selectedSerializedNodes, setSelectedSerializedNodes] = useState<BaseSerializedNode[]>([])
useEffect(() => {
@ -216,7 +217,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
abortActiveStreams()
const conversation = await getChatMessagesById(conversationId)
if (!conversation) {
throw new Error('Conversation not found')
throw new Error(t('chat.errors.conversationNotFound'))
}
setCurrentConversationId(conversationId)
setChatMessages(conversation)
@ -227,8 +228,8 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
type: 'idle',
})
} catch (error) {
new Notice('Failed to load conversation')
console.error('Failed to load conversation', error)
new Notice(t('chat.errors.failedToLoadConversation'))
console.error(t('chat.errors.failedToLoadConversation'), error)
}
}
@ -1031,7 +1032,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
{submitMutation.isPending && (
<button onClick={abortActiveStreams} className="infio-stop-gen-btn">
<CircleStop size={16} />
<div>Stop generation</div>
<div>{t('chat.stop')}</div>
</button>
)}
</div>

View File

@ -8,6 +8,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { TemplateContent } from '../../database/schema'
import { useCommands } from '../../hooks/use-commands'
import { t } from '../../lang/helpers'
import LexicalContentEditable from './chat-input/LexicalContentEditable'
@ -107,11 +108,11 @@ const CommandsView = (
const serializedEditorState = editorRef.current.toJSON()
const nodes = serializedEditorState.editorState.root.children
if (nodes.length === 0) {
new Notice('Please enter a content for your template')
new Notice(String(t('command.errorContentRequired')))
return
}
if (newCommandName.trim().length === 0) {
new Notice('Please enter a name for your template')
new Notice(String(t('command.errorNameRequired')))
return
}
@ -140,13 +141,13 @@ const CommandsView = (
const nameInput = nameInputRefs.current.get(id)
const currContentEditorRef = contentEditorRefs.current.get(id)
if (!currContentEditorRef) {
new Notice('Please enter a content for your template')
new Notice(String(t('command.errorContentRequired')))
return
}
const serializedEditorState = currContentEditorRef.toJSON()
const nodes = serializedEditorState.editorState.root.children
if (nodes.length === 0) {
new Notice('Please enter a content for your template')
new Notice(String(t('command.errorContentRequired')))
return
}
await updateCommand(
@ -190,15 +191,15 @@ const CommandsView = (
{/* 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>
<h2 className="infio-commands-header-title">{t('command.createQuickCommand')}</h2>
<div className="infio-commands-label">{t('command.name')}</div>
<input
type="text"
value={newCommandName}
onChange={(e) => setNewCommandName(e.target.value)}
className="infio-commands-input"
/>
<div className="infio-commands-label">Content</div>
<div className="infio-commands-label">{t('command.content')}</div>
<div className="infio-commands-textarea">
<LexicalContentEditable
initialEditorState={initialEditorState}
@ -211,7 +212,7 @@ const CommandsView = (
className="infio-commands-add-btn"
disabled={!newCommandName.trim()}
>
<span>Create Command</span>
<span>{t('command.createCommand')}</span>
</button>
</div>
</div>
@ -221,7 +222,7 @@ const CommandsView = (
<Search size={18} className="infio-commands-search-icon" />
<input
type="text"
placeholder="Search Command..."
placeholder={t('command.searchPlaceholder')}
value={searchTerm}
onChange={handleSearch}
className="infio-commands-search-input"
@ -232,7 +233,7 @@ const CommandsView = (
<div className="infio-commands-list">
{filteredCommands.length === 0 ? (
<div className="infio-commands-empty">
<p>No commands found</p>
<p>{t('command.noCommandsFound')}</p>
</div>
) : (
filteredCommands.map(command => (
@ -260,7 +261,7 @@ const CommandsView = (
onClick={() => handleSaveEdit(command.id)}
className="infio-commands-add-btn"
>
<span>Update Command</span>
<span>{t('command.updateCommand')}</span>
</button>
</div>
</div>

View File

@ -9,13 +9,12 @@ import { useRAG } from '../../contexts/RAGContext';
import { useSettings } from '../../contexts/SettingsContext';
import { CustomMode, GroupEntry, ToolGroup } from '../../database/json/custom-mode/types';
import { useCustomModes } from '../../hooks/use-custom-mode';
import { t } from '../../lang/helpers';
import { PreviewView, PreviewViewState } from '../../PreviewView';
import { modes as buildinModes } from '../../utils/modes';
import { openOrCreateMarkdownFile } from '../../utils/obsidian';
import { PromptGenerator, getFullLanguageName } from '../../utils/prompt-generator';
import { t } from '../../lang/helpers';
const CustomModeView = () => {
const app = useApp()

View File

@ -8,6 +8,7 @@ import {
Info,
} from 'lucide-react'
import { t } from '../../lang/helpers'
import { ResponseUsage } from '../../types/llm/response'
type LLMResponseInfoProps = {
@ -30,27 +31,27 @@ export default function LLMResponseInfoPopover({
</Popover.Trigger>
{usage ? (
<Popover.Content className="infio-chat-popover-content infio-llm-info-content">
<div className="infio-llm-info-header">LLM response information</div>
<div className="infio-llm-info-header">{t('chat.LLMResponseInfoPopover.header')}</div>
<div className="infio-llm-info-tokens">
<div className="infio-llm-info-tokens-header">Token count</div>
<div className="infio-llm-info-tokens-header">{t('chat.LLMResponseInfoPopover.tokenCount')}</div>
<div className="infio-llm-info-tokens-grid">
<div className="infio-llm-info-token-row">
<ArrowUp className="infio-llm-info-icon--input" />
<span>Input:</span>
<span>{t('chat.LLMResponseInfoPopover.promptTokens')}</span>
<span className="infio-llm-info-token-value">
{usage.prompt_tokens}
</span>
</div>
<div className="infio-llm-info-token-row">
<ArrowDown className="infio-llm-info-icon--output" />
<span>Output:</span>
<span>{t('chat.LLMResponseInfoPopover.completionTokens')}</span>
<span className="infio-llm-info-token-value">
{usage.completion_tokens}
</span>
</div>
<div className="infio-llm-info-token-row infio-llm-info-token-total">
<ArrowRightLeft className="infio-llm-info-icon--total" />
<span>Total:</span>
<span>{t('chat.LLMResponseInfoPopover.totalTokens')}</span>
<span className="infio-llm-info-token-value">
{usage.total_tokens}
</span>
@ -59,24 +60,24 @@ export default function LLMResponseInfoPopover({
</div>
<div className="infio-llm-info-footer-row">
<Coins className="infio-llm-info-icon--footer" />
<span>Estimated price:</span>
<span>{t('chat.LLMResponseInfoPopover.estimatedPrice')}</span>
<span className="infio-llm-info-footer-value">
{estimatedPrice === null
? 'Not available'
? t('chat.LLMResponseInfoPopover.notAvailable')
: `$${estimatedPrice.toFixed(4)}`}
</span>
</div>
<div className="infio-llm-info-footer-row">
<Cpu className="infio-llm-info-icon--footer" />
<span>Model:</span>
<span>{t('chat.LLMResponseInfoPopover.model')}</span>
<span className="infio-llm-info-footer-value infio-llm-info-model">
{model ?? 'Not available'}
{model ?? t('chat.LLMResponseInfoPopover.notAvailable')}
</span>
</div>
</Popover.Content>
) : (
<Popover.Content className="infio-chat-popover-content">
<div>Usage statistics are not available for this model</div>
<div>{t('chat.LLMResponseInfoPopover.usageNotAvailable')}</div>
</Popover.Content>
)}
</Popover.Root>

View File

@ -2,6 +2,7 @@ import { Check, Diff, Loader2, X } from 'lucide-react'
import { PropsWithChildren, useState } from 'react'
import { useDarkModeContext } from "../../../contexts/DarkModeContext"
import { t } from '../../../lang/helpers'
import { ApplyStatus, ToolArgs } from "../../../types/apply"
import { MemoizedSyntaxHighlighterWrapper } from "./SyntaxHighlighterWrapper"
@ -55,23 +56,23 @@ export default function MarkdownApplyDiffBlock({
{
!finish ? (
<>
<Loader2 className="spinner" size={14} /> Loading...
<Loader2 className="spinner" size={14} /> {t('chat.reactMarkdown.loading')}
</>
) : applyStatus === ApplyStatus.Idle ? (
applying ? (
<>
<Loader2 className="spinner" size={14} /> Applying...
<Loader2 className="spinner" size={14} /> {t('chat.reactMarkdown.applying')}
</>
) : (
'Apply'
t('chat.reactMarkdown.apply')
)
) : applyStatus === ApplyStatus.Applied ? (
<>
<Check size={14} /> Success
<Check size={14} /> {t('chat.reactMarkdown.success')}
</>
) : (
<>
<X size={14} /> Failed
<X size={14} /> {t('chat.reactMarkdown.failed')}
</>
)}
</button>

View File

@ -2,6 +2,7 @@ import { Check, CopyIcon, Edit, Loader2, X } from 'lucide-react'
import { PropsWithChildren, useMemo, useState } from 'react'
import { useDarkModeContext } from "../../../contexts/DarkModeContext"
import { t } from '../../../lang/helpers'
import { ApplyStatus, ToolArgs } from "../../../types/apply"
import { MemoizedSyntaxHighlighterWrapper } from "./SyntaxHighlighterWrapper"
@ -63,7 +64,7 @@ export default function MarkdownEditFileBlock({
{path && (
<div className={'infio-chat-code-block-header-filename'}>
<Edit size={10} className="infio-chat-code-block-header-icon" />
{mode}: {path}
{t('chat.reactMarkdown.editOrApplyDiff').replace('{mode}', mode).replace('{path}', path)}
</div>
)}
<div className={'infio-chat-code-block-header-button'}>
@ -74,34 +75,34 @@ export default function MarkdownEditFileBlock({
>
{copied ? (
<>
<Check size={10} /> Copied
<Check size={10} /> {t('chat.reactMarkdown.copied')}
</>
) : (
<>
<CopyIcon size={10} /> Copy
<CopyIcon size={10} /> {t('chat.reactMarkdown.copy')}
</>
)}
</button>
<button
onClick={handleApply}
style={{ color: '#008000' }}
className="infio-apply-button"
disabled={applyStatus !== ApplyStatus.Idle || applying}
>
{applyStatus === ApplyStatus.Idle ? (
applying ? (
<>
<Loader2 className="spinner" size={14} /> Applying...
<Loader2 className="spinner" size={14} /> {t('chat.reactMarkdown.applying')}
</>
) : (
'Apply'
t('chat.reactMarkdown.apply')
)
) : applyStatus === ApplyStatus.Applied ? (
<>
<Check size={14} /> Success
<Check size={14} /> {t('chat.reactMarkdown.success')}
</>
) : (
<>
<X size={14} /> Failed
<X size={14} /> {t('chat.reactMarkdown.failed')}
</>
)}
</button>

View File

@ -1,6 +1,7 @@
import { Check, ChevronDown, ChevronRight, Globe, Loader2, X } from 'lucide-react'
import React, { useEffect, useRef, useState } from 'react'
import { t } from '../../../lang/helpers'
import { ApplyStatus, FetchUrlsContentToolArgs } from "../../../types/apply"
export default function MarkdownFetchUrlsContentBlock({
@ -38,7 +39,7 @@ export default function MarkdownFetchUrlsContentBlock({
<div className="infio-chat-code-block-header">
<div className="infio-chat-code-block-header-filename">
<Globe size={10} className="infio-chat-code-block-header-icon" />
Fetch URLs Content
{t('chat.reactMarkdown.fetchUrlsContent')}
</div>
<div className="infio-chat-code-block-header-button">
<button
@ -48,15 +49,15 @@ export default function MarkdownFetchUrlsContentBlock({
{
!finish || applyStatus === ApplyStatus.Idle ? (
<>
<Loader2 className="spinner" size={14} /> Fetching...
<Loader2 className="spinner" size={14} /> {t('chat.reactMarkdown.fetching')}
</>
) : applyStatus === ApplyStatus.Applied ? (
<>
<Check size={14} /> Done
<Check size={14} /> {t('chat.reactMarkdown.done')}
</>
) : (
<>
<X size={14} /> Failed
<X size={14} /> {t('chat.reactMarkdown.failed')}
</>
)}
</button>

View File

@ -2,6 +2,7 @@ import { FolderOpen } from 'lucide-react'
import React from 'react'
import { useApp } from "../../../contexts/AppContext"
import { t } from '../../../lang/helpers'
import { ApplyStatus, ListFilesToolArgs } from "../../../types/apply"
import { openMarkdownFile } from "../../../utils/obsidian"
@ -42,7 +43,7 @@ export default function MarkdownListFilesBlock({
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<FolderOpen size={14} className="infio-chat-code-block-header-icon" />
List files: {path}
{t('chat.reactMarkdown.listFiles').replace('{path}', path)}
</div>
</div>
</div>

View File

@ -2,6 +2,7 @@ import { ExternalLink } from 'lucide-react'
import React from 'react'
import { useApp } from "../../../contexts/AppContext"
import { t } from '../../../lang/helpers'
import { ApplyStatus, ReadFileToolArgs } from "../../../types/apply"
import { openMarkdownFile } from "../../../utils/obsidian"
@ -39,7 +40,7 @@ export default function MarkdownReadFileBlock({
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<ExternalLink size={10} className="infio-chat-code-block-header-icon" />
Read file: {path}
{t('chat.reactMarkdown.readFile').replace('{path}', path)}
</div>
</div>
</div>

View File

@ -2,6 +2,7 @@ import { ChevronDown, ChevronRight, Brain } from 'lucide-react'
import { PropsWithChildren, useEffect, useRef, useState } from 'react'
import { useDarkModeContext } from "../../../contexts/DarkModeContext"
import { t } from '../../../lang/helpers'
import { MemoizedSyntaxHighlighterWrapper } from "./SyntaxHighlighterWrapper"
@ -28,7 +29,7 @@ export default function MarkdownReasoningBlock({
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<Brain size={10} className="infio-chat-code-block-header-icon" />
Reasoning
{t('chat.reactMarkdown.reasoning')}
</div>
<button
className="clickable-icon infio-chat-list-dropdown"

View File

@ -2,6 +2,7 @@ import { FileSearch } from 'lucide-react'
import React from 'react'
import { useApp } from "../../../contexts/AppContext"
import { t } from '../../../lang/helpers'
import { ApplyStatus, RegexSearchFilesToolArgs } from "../../../types/apply"
import { openMarkdownFile } from "../../../utils/obsidian"
@ -43,7 +44,7 @@ export default function MarkdownRegexSearchFilesBlock({
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<FileSearch size={14} className="infio-chat-code-block-header-icon" />
<span>regex search files &quot;{regex}&quot; in {path}</span>
<span>{t('chat.reactMarkdown.regexSearchInPath').replace('{regex}', regex).replace('{path}', path)}</span>
</div>
</div>
</div>

View File

@ -3,6 +3,7 @@ import React from 'react'
import { useApp } from '../../../contexts/AppContext'
import { useDarkModeContext } from '../../../contexts/DarkModeContext'
import { t } from '../../../lang/helpers'
import { ApplyStatus, SearchAndReplaceToolArgs } from '../../../types/apply'
import { openMarkdownFile } from '../../../utils/obsidian'
@ -52,7 +53,7 @@ export default function MarkdownSearchAndReplace({
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<Replace size={10} className="infio-chat-code-block-header-icon" />
Search and replace in {path}
{t('chat.reactMarkdown.searchAndReplaceInPath').replace('{path}', path)}
</div>
<div className={'infio-chat-code-block-header-button'}>
<button
@ -66,18 +67,18 @@ export default function MarkdownSearchAndReplace({
) : applyStatus === ApplyStatus.Idle ? (
applying ? (
<>
<Loader2 className="spinner" size={14} /> Applying...
<Loader2 className="spinner" size={14} /> {t('chat.reactMarkdown.applying')}
</>
) : (
'Apply'
t('chat.reactMarkdown.apply')
)
) : applyStatus === ApplyStatus.Applied ? (
<>
<Check size={14} /> Success
<Check size={14} /> {t('chat.reactMarkdown.success')}
</>
) : (
<>
<X size={14} /> Failed
<X size={14} /> {t('chat.reactMarkdown.failed')}
</>
)}
</button>

View File

@ -2,6 +2,7 @@ import { Check, Loader2, Search, X } from 'lucide-react'
import React from 'react'
import { useSettings } from "../../../contexts/SettingsContext"
import { t } from '../../../lang/helpers'
import { ApplyStatus, SearchWebToolArgs } from "../../../types/apply"
export default function MarkdownWebSearchBlock({
@ -46,7 +47,7 @@ export default function MarkdownWebSearchBlock({
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<Search size={14} className="infio-chat-code-block-header-icon" />
Web search: {query}
{t('chat.reactMarkdown.webSearch').replace('{query}', query)}
</div>
<div className={'infio-chat-code-block-header-button'}>
<button
@ -56,15 +57,15 @@ export default function MarkdownWebSearchBlock({
{
!finish || applyStatus === ApplyStatus.Idle ? (
<>
<Loader2 className="spinner" size={14} /> Searching...
<Loader2 className="spinner" size={14} /> {t('chat.reactMarkdown.searching')}
</>
) : applyStatus === ApplyStatus.Applied ? (
<>
<Check size={14} /> Done
<Check size={14} /> {t('chat.reactMarkdown.done')}
</>
) : (
<>
<X size={14} /> Failed
<X size={14} /> {t('chat.reactMarkdown.failed')}
</>
)}
</button>

View File

@ -2,6 +2,7 @@ import { FileSearch } from 'lucide-react'
import React from 'react'
import { useApp } from "../../../contexts/AppContext"
import { t } from '../../../lang/helpers'
import { ApplyStatus, SemanticSearchFilesToolArgs } from "../../../types/apply"
import { openMarkdownFile } from "../../../utils/obsidian"
@ -42,7 +43,7 @@ export default function MarkdownSemanticSearchFilesBlock({
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<FileSearch size={14} className="infio-chat-code-block-header-icon" />
<span>semantic search files &quot;{query}&quot; in {path}</span>
<span>{t('chat.reactMarkdown.semanticSearchInPath').replace('{query}', query).replace('{path}', path)}</span>
</div>
</div>
</div>

View File

@ -2,6 +2,7 @@ import { Check, Loader2, Settings2, X } from 'lucide-react'
import { PropsWithChildren, useState } from 'react'
import { useDarkModeContext } from "../../../contexts/DarkModeContext"
import { t } from '../../../lang/helpers'
import { ApplyStatus, ToolArgs } from "../../../types/apply"
import { MemoizedSyntaxHighlighterWrapper } from "./SyntaxHighlighterWrapper"
@ -40,7 +41,7 @@ export default function MarkdownSwitchModeBlock({
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<Settings2 size={10} className="infio-chat-code-block-header-icon" />
Switch to &quot;{mode.charAt(0).toUpperCase() + mode.slice(1)}&quot; mode
{t('chat.reactMarkdown.switchToMode').replace('{mode}', mode.charAt(0).toUpperCase() + mode.slice(1))}
</div>
<div className={'infio-chat-code-block-header-button'}>
<button
@ -51,18 +52,18 @@ export default function MarkdownSwitchModeBlock({
{applyStatus === ApplyStatus.Idle ? (
applying ? (
<>
<Loader2 className="spinner" size={14} /> Allowing...
<Loader2 className="spinner" size={14} /> {t('chat.reactMarkdown.allowing')}
</>
) : (
'Allow'
t('chat.reactMarkdown.allow')
)
) : applyStatus === ApplyStatus.Applied ? (
<>
<Check size={14} /> Success
<Check size={14} /> {t('chat.reactMarkdown.success')}
</>
) : (
<>
<X size={14} /> Failed
<X size={14} /> {t('chat.reactMarkdown.failed')}
</>
)}
</button>

View File

@ -5,6 +5,8 @@ import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import { useApp } from 'src/contexts/AppContext';
import { t } from '../../../lang/helpers'
function CopyButton({ message }: { message: string }) {
const [copied, setCopied] = useState(false)
@ -33,7 +35,7 @@ function CopyButton({ message }: { message: string }) {
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="infio-tooltip-content">
Copy message
{t('chat.reactMarkdown.copyMsg')}
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
@ -77,7 +79,7 @@ function CreateNewFileButton({ message }: { message: string }) {
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="infio-tooltip-content">
Create new note
{t('chat.reactMarkdown.createNewNote')}
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
@ -123,9 +125,9 @@ const MarkdownWithIcons = ({
switch (iconName) {
case 'ask_followup_question':
return 'Ask Followup Question:';
return t('chat.reactMarkdown.askFollowupQuestion');
case 'attempt_completion':
return 'Task Completion';
return t('chat.reactMarkdown.taskCompletion');
default:
return null;
}

View File

@ -1,5 +1,5 @@
import { SelectVector } from '../../database/schema'
import { t } from '../../lang/helpers'
export type QueryProgressState =
| {
type: 'reading-mentionables'
@ -38,7 +38,7 @@ export default function QueryProgress({
return (
<div className="infio-query-progress">
<p>
Reading mentioned files
{t('chat.queryProgress.readingMentionableFiles')}
<DotLoader />
</p>
</div>
@ -47,17 +47,17 @@ export default function QueryProgress({
return (
<div className="infio-query-progress">
<p>
{`Indexing ${state.indexProgress.totalFiles} file`}
{`${t('chat.queryProgress.indexing')} ${state.indexProgress.totalFiles} ${t('chat.queryProgress.file')}`}
<DotLoader />
</p>
<p className="infio-query-progress-detail">{`${state.indexProgress.completedChunks}/${state.indexProgress.totalChunks} chunks indexed`}</p>
<p className="infio-query-progress-detail">{`${state.indexProgress.completedChunks}/${state.indexProgress.totalChunks} ${t('chat.queryProgress.chunkIndexed')}`}</p>
</div>
)
case 'querying':
return (
<div className="infio-query-progress">
<p>
Querying the vault
{t('chat.queryProgress.queryingVault')}
<DotLoader />
</p>
</div>
@ -66,7 +66,7 @@ export default function QueryProgress({
return (
<div className="infio-query-progress">
<p>
Reading related files
{t('chat.queryProgress.readingRelatedFiles')}
<DotLoader />
</p>
{state.queryResult.map((result) => (

View File

@ -1,20 +1,22 @@
import { Platform } from 'obsidian';
import React from 'react';
import { t } from '../../lang/helpers'
const ShortcutInfo: React.FC = () => {
const modKey = Platform.isMacOS ? 'Cmd' : 'Ctrl';
const shortcuts = [
{
label: 'Edit inline',
label: t('chat.shortcutInfo.editInline'),
shortcut: `${modKey}+Shift+K`,
},
{
label: 'Chat with select',
label: t('chat.shortcutInfo.chatWithSelect'),
shortcut: `${modKey}+Shift+L`,
},
{
label: 'Submit with vault',
label: t('chat.shortcutInfo.submitWithVault'),
shortcut: `${modKey}+Shift+Enter`,
}
];

View File

@ -5,67 +5,69 @@ import { useState } from 'react'
import { useApp } from '../../contexts/AppContext'
import { SelectVector } from '../../database/schema'
import { t } from '../../lang/helpers'
import { openMarkdownFile } from '../../utils/obsidian'
function SimiliartySearchItem({
chunk,
chunk,
}: {
chunk: Omit<SelectVector, 'embedding'> & {
similarity: number
}
chunk: Omit<SelectVector, 'embedding'> & {
similarity: number
}
}) {
const app = useApp()
const app = useApp()
const handleClick = () => {
openMarkdownFile(app, chunk.path, chunk.metadata.startLine)
}
return (
<div onClick={handleClick} className="infio-similarity-search-item">
<div className="infio-similarity-search-item__similarity">
{chunk.similarity.toFixed(3)}
</div>
<div className="infio-similarity-search-item__path">
{path.basename(chunk.path)}
</div>
<div className="infio-similarity-search-item__line-numbers">
{`${chunk.metadata.startLine} - ${chunk.metadata.endLine}`}
</div>
</div>
)
const handleClick = () => {
openMarkdownFile(app, chunk.path, chunk.metadata.startLine)
}
return (
<div onClick={handleClick} className="infio-similarity-search-item">
<div className="infio-similarity-search-item__similarity">
{chunk.similarity.toFixed(3)}
</div>
<div className="infio-similarity-search-item__path">
{path.basename(chunk.path)}
</div>
<div className="infio-similarity-search-item__line-numbers">
{`${chunk.metadata.startLine} - ${chunk.metadata.endLine}`}
</div>
</div>
)
}
export default function SimilaritySearchResults({
similaritySearchResults,
similaritySearchResults,
}: {
similaritySearchResults: (Omit<SelectVector, 'embedding'> & {
similarity: number
})[]
similaritySearchResults: (Omit<SelectVector, 'embedding'> & {
similarity: number
})[]
}) {
const [isOpen, setIsOpen] = useState(false)
const [isOpen, setIsOpen] = useState(false)
return (
<div className="infio-similarity-search-results">
<div
onClick={() => {
setIsOpen(!isOpen)
}}
className="infio-similarity-search-results__trigger"
>
{isOpen ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
<div>Show Referenced Documents ({similaritySearchResults.length})</div>
</div>
{isOpen && (
<div
style={{
display: 'flex',
flexDirection: 'column',
}}
>
{similaritySearchResults.map((chunk) => (
<SimiliartySearchItem key={chunk.id} chunk={chunk} />
))}
</div>
)}
</div>
)
return (
<div className="infio-similarity-search-results">
<div
onClick={() => {
setIsOpen(!isOpen)
}}
className="infio-similarity-search-results__trigger"
>
{isOpen ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
<div>
{t('chat.searchResults.showReferencedDocuments')} ({similaritySearchResults.length})</div>
</div>
{isOpen && (
<div
style={{
display: 'flex',
flexDirection: 'column',
}}
>
{similaritySearchResults.map((chunk) => (
<SimiliartySearchItem key={chunk.id} chunk={chunk} />
))}
</div>
)}
</div>
)
}

View File

@ -2,6 +2,7 @@ import { ImageIcon } from 'lucide-react'
import { TFile } from 'obsidian'
import { useApp } from '../../../contexts/AppContext'
import { t } from '../../../lang/helpers'
import { ImageSelectorModal } from '../../modals/ImageSelectorModal'
export function ImageUploadButton({
@ -34,7 +35,7 @@ export function ImageUploadButton({
<div className="infio-chat-user-input-submit-button-icons">
<ImageIcon size={12} />
</div>
<div>Image</div>
<div>{t('chat.input.image')}</div>
</button>
)
}

View File

@ -4,6 +4,7 @@ import { ChevronDown, ChevronUp, Star, StarOff } from 'lucide-react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useSettings } from '../../../contexts/SettingsContext'
import { t } from '../../../lang/helpers'
import { ApiProvider } from '../../../types/llm/model'
import { GetAllProviders, GetProviderModelIds } from "../../../utils/api"
@ -256,7 +257,7 @@ export function ModelSelect() {
{settings.collectedChatModels?.length > 0 && (
<div className="infio-model-section">
<div className="infio-model-section-title">
<Star size={12} className="infio-star-active" /> collected models
<Star size={12} className="infio-star-active" /> {t('chat.input.collectedModels')}
</div>
<ul className="infio-collected-models-list">
{settings.collectedChatModels.map((collectedModel, index) => (
@ -415,7 +416,7 @@ export function ModelSelect() {
)}
</div>
{isLoading ? (
<div className="infio-loading">loading...</div>
<div className="infio-loading">{t('chat.input.loading')}</div>
) : (
<div className="infio-model-section">
<ul>

View File

@ -1,9 +1,11 @@
import { CornerDownLeftIcon } from 'lucide-react'
import { t } from '../../../lang/helpers'
export function SubmitButton({ onClick }: { onClick: () => void }) {
return (
<button className="infio-chat-user-input-submit-button" onClick={onClick}>
<div>submit</div>
{t('chat.input.submit')}
<div className="infio-chat-user-input-submit-button-icons">
<CornerDownLeftIcon size={12} />
</div>

View File

@ -8,6 +8,8 @@ import {
} from 'lexical'
import { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'
import { t } from '../../../../../lang/helpers'
export default function CreateCommandPopoverPlugin({
anchorElement,
contentEditableElement,
@ -121,7 +123,7 @@ export default function CreateCommandPopoverPlugin({
setIsPopoverOpen(false)
}}
>
create command
{t('chat.input.createCommand')}
</button>
)
}

View File

@ -4,6 +4,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { APPLY_VIEW_TYPE } from '../../constants';
import LLMManager from '../../core/llm/manager';
import { t } from '../../lang/helpers';
import { InfioSettings } from '../../types/settings';
import { GetProviderModelIds } from '../../utils/api';
import { ApplyEditToFile } from '../../utils/apply';
@ -52,7 +53,7 @@ const InputArea: React.FC<InputAreaProps> = ({ value, onChange, handleSubmit, ha
<textarea
ref={textareaRef}
className="infio-ai-block-content"
placeholder="Input instruction, Enter to submit, Esc to close"
placeholder={t('inlineEdit.placeholder')}
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={handleKeyDown}
@ -83,8 +84,9 @@ const ControlArea: React.FC<ControlAreaProps> = ({
try {
const models = await GetProviderModelIds(settings.chatModelProvider);
setProviderModels(models);
} catch (error) {
console.error("Failed to fetch provider models:", error);
} catch (err) {
const error = err as Error;
console.error(t("inlineEdit.fetchModelsError"), error.message);
}
};
fetchModels();
@ -109,9 +111,9 @@ const ControlArea: React.FC<ControlAreaProps> = ({
onClick={onSubmit}
disabled={isSubmitting}
>
{isSubmitting ? "submitting..." : (
{isSubmitting ? t("inlineEdit.submitting") : (
<>
submit
{t("inlineEdit.submit")}
<CornerDownLeft size={11} className="infio-ai-block-submit-icon" />
</>
)}
@ -134,7 +136,7 @@ export const InlineEdit: React.FC<InlineEditProps> = ({
const promptGenerator = new PromptGenerator(
async () => {
throw new Error("RAG not needed for inline edit");
throw new Error(t("inlineEdit.ragNotNeeded"));
},
plugin.app,
settings
@ -153,19 +155,19 @@ export const InlineEdit: React.FC<InlineEditProps> = ({
const getActiveContext = async () => {
const activeFile = plugin.app.workspace.getActiveFile();
if (!activeFile) {
console.error("No active file");
console.error(t("inlineEdit.noActiveFile"));
return {};
}
const editor = plugin.app.workspace.getActiveViewOfType(MarkdownView)?.editor;
if (!editor) {
console.error("No active editor");
console.error(t("inlineEdit.noActiveEditor"));
return { activeFile };
}
const selection = editor.getSelection();
if (!selection) {
console.error("No text selected");
console.error(t("inlineEdit.noTextSelected"));
return { activeFile, editor };
}
@ -190,7 +192,7 @@ export const InlineEdit: React.FC<InlineEditProps> = ({
try {
const { activeFile, editor, selection } = await getActiveContext();
if (!activeFile || !editor || !selection) {
console.error("No active file, editor, or selection");
console.error(t("inlineEdit.noActiveContext"));
setIsSubmitting(false);
return;
}
@ -201,7 +203,7 @@ export const InlineEdit: React.FC<InlineEditProps> = ({
};
if (!chatModel) {
setIsSubmitting(false);
throw new Error("Invalid chat model");
throw new Error(t("inlineEdit.invalidChatModel"));
}
const from = editor.getCursor("from");
@ -238,7 +240,7 @@ export const InlineEdit: React.FC<InlineEditProps> = ({
}
if (!response_content) {
setIsSubmitting(false);
throw new Error("Empty response from LLM");
throw new Error(t("inlineEdit.emptyLLMResponse"));
}
const parsedBlock = parseSmartComposeBlock(
@ -248,14 +250,15 @@ export const InlineEdit: React.FC<InlineEditProps> = ({
if (!activeFile || !(activeFile.path && typeof activeFile.path === 'string')) {
setIsSubmitting(false);
throw new Error("Invalid active file");
throw new Error(t("inlineEdit.invalidActiveFile"));
}
let fileContent: string;
try {
fileContent = await plugin.app.vault.cachedRead(activeFile);
} catch (error) {
console.error("Failed to read file:", error);
} catch (err) {
const error = err as Error;
console.error(t("inlineEdit.readFileError"), error.message);
setIsSubmitting(false);
return;
}
@ -268,7 +271,7 @@ export const InlineEdit: React.FC<InlineEditProps> = ({
);
if (!updatedContent) {
console.error("Failed to apply changes");
console.error(t("inlineEdit.applyChangesError"));
setIsSubmitting(false);
return;
}
@ -283,8 +286,9 @@ export const InlineEdit: React.FC<InlineEditProps> = ({
newContent: removeAITags(updatedContent),
},
});
} catch (error) {
console.error("Error in inline edit:", error);
} catch (err) {
const error = err as Error;
console.error(t("inlineEdit.inlineEditError"), error.message);
} finally {
setIsSubmitting(false);
}

View File

@ -1,4 +1,106 @@
export default {
chat: {
stop: "Stop",
errors: {
failedToLoadConversation: "Failed to load conversation",
failedToSaveHistory: "Failed to save chat history",
failedToApplyChanges: "Failed to apply changes",
conversationNotFound: "Conversation not found",
fileNotFound: "File not found: {{path}}",
failedToApplyEditChanges: "Failed to apply edit changes",
failedToSearchAndReplace: "Failed to search and replace"
},
apply: {
changesApplied: "Changes successfully applied",
changesRejected: "User rejected changes"
},
search: {
noResultsFound: "No results found for '{{query}}'"
},
history: {
noConversations: "No conversations"
},
shortcutInfo: {
editInline: "Edit inline",
chatWithSelect: "Chat with selected text",
submitWithVault: "Submit with vault"
},
searchResults: {
showReferencedDocuments: "Show Referenced Documents"
},
LLMResponseInfoPopover: {
header: "LLM response information",
tokenCount: "Token count",
promptTokens: "Prompt tokens",
completionTokens: "Completion tokens",
totalTokens: "Total tokens",
model: "Model",
estimatedPrice: "Estimated price",
usageNotAvailable: "Usage statistics are not available for this model",
notAvailable: "Not available"
},
queryProgress: {
readingMentionableFiles: "Reading mentioned files",
indexing: "Indexing",
file: "file",
chunkIndexed: "chunk indexed",
queryingVault: "Querying the vault",
readingRelatedFiles: "Reading related files"
},
reactMarkdown: {
allow: "Allow",
allowing: "Allowing...",
success: "Success",
failed: "Failed",
switchToMode: 'Switch to "{mode}" mode',
semanticSearchInPath: 'semantic search files "{query}" in {path}',
webSearch: "Web search: {query}",
searching: "Searching...",
done: "Done",
searchAndReplaceInPath: "Search and replace in {path}",
applying: "Applying...",
apply: "Apply",
reasoning: "Reasoning",
readFile: "Read file: {path}",
listFiles: "List files: {path}",
fetchUrlsContent: "Fetch URLs Content",
fetching: "Fetching...",
copied: "Copied",
copy: "Copy",
editOrApplyDiff: "{mode}: {path}",
loading: "Loading...",
regexSearchInPath: 'regex search files "{regex}" in {path}',
createNewNote: "Create new note",
copyMsg: "Copy message",
taskCompletion: "Task Completion",
askFollowupQuestion: "Ask Followup Question:",
viewDetails: "View details"
},
input: {
submit: "Submit",
collectedModels: "Collected Models",
loading: "Loading...",
image: "Image",
createCommand: "Create Command"
}
},
inlineEdit: {
placeholder: "Input instruction, Enter to submit, Esc to close",
fetchModelsError: "Failed to fetch provider models:",
submitting: "submitting...",
submit: "submit",
ragNotNeeded: "RAG not needed for inline edit",
noActiveFile: "No active file",
noActiveEditor: "No active editor",
noTextSelected: "No text selected",
noActiveContext: "No active file, editor, or selection",
invalidChatModel: "Invalid chat model",
emptyLLMResponse: "Empty response from LLM",
invalidActiveFile: "Invalid active file",
readFileError: "Failed to read file:",
applyChangesError: "Failed to apply changes",
inlineEditError: "Error in inline edit:",
},
prompt: {
"title": "Prompts",
"description": "Click + to create a new mode",
@ -25,5 +127,16 @@ export default {
"overrideWarning": ". This is a very advanced feature that will override all built-in prompts including tool usage, please use with caution",
"previewSystemPrompt": "Preview System Prompt",
"save": "Save"
},
command: {
"createQuickCommand": "Create Quick Command",
"name": "Name",
"content": "Content",
"createCommand": "Create Command",
"searchPlaceholder": "Search Command...",
"noCommandsFound": "No commands found",
"updateCommand": "Update Command",
"errorContentRequired": "Please enter a content for your template",
"errorNameRequired": "Please enter a name for your template"
}
}

View File

@ -2,6 +2,108 @@
// 简体中文
export default {
chat: {
stop: "停止",
errors: {
failedToLoadConversation: "加载对话失败",
failedToSaveHistory: "保存聊天记录失败",
failedToApplyChanges: "应用更改失败",
conversationNotFound: "未找到对话",
fileNotFound: "未找到文件:{{path}}",
failedToApplyEditChanges: "应用编辑更改失败",
failedToSearchAndReplace: "搜索和替换失败"
},
apply: {
changesApplied: "更改已成功应用",
changesRejected: "用户拒绝了更改"
},
search: {
noResultsFound: "未找到 '{{query}}' 的结果"
},
history: {
noConversations: "没有对话"
},
shortcutInfo: {
editInline: "行内编辑",
chatWithSelect: "与选定文本聊天",
submitWithVault: "使用 Vault 提交"
},
searchResults: {
showReferencedDocuments: "显示引用的文档"
},
LLMResponseInfoPopover: {
header: "LLM 响应信息",
tokenCount: "Token 数量",
promptTokens: "提示 Tokens",
completionTokens: "补全 Tokens",
totalTokens: "总 Tokens",
model: "模型",
estimatedPrice: "预估价格",
usageNotAvailable: "此模型无法获取使用统计信息",
notAvailable: "不可用"
},
queryProgress: {
readingMentionableFiles: "正在读取提及的文件",
indexing: "正在索引",
file: "文件",
chunkIndexed: "块已索引",
queryingVault: "正在查询 Vault",
readingRelatedFiles: "正在读取相关文件"
},
reactMarkdown: {
allow: "允许",
allowing: "正在允许...",
success: "成功",
failed: "失败",
switchToMode: '切换到 "{mode}" 模式',
semanticSearchInPath: '在 {path} 中语义搜索文件 "{query}"',
webSearch: "网页搜索:{query}",
searching: "正在搜索...",
done: "完成",
searchAndReplaceInPath: "在 {path} 中搜索和替换",
applying: "正在应用...",
apply: "应用",
reasoning: "推理",
readFile: "读取文件:{path}",
listFiles: "列出文件:{path}",
fetchUrlsContent: "获取 URL 内容",
fetching: "正在获取...",
copied: "已复制",
copy: "复制",
editOrApplyDiff: "{mode}{path}",
loading: "加载中...",
regexSearchInPath: '在 {path} 中正则搜索文件 "{regex}"',
createNewNote: "创建新笔记",
copyMsg: "复制消息",
taskCompletion: "任务完成",
askFollowupQuestion: "询问后续问题:",
viewDetails: "查看详情"
},
input: {
submit: "提交",
collectedModels: "收集的模型",
loading: "加载中...",
image: "图片",
createCommand: "创建命令"
}
},
inlineEdit: {
placeholder: "输入指令Enter 提交Esc 关闭",
fetchModelsError: "获取 Provider 模型失败:",
submitting: "提交中...",
submit: "提交",
ragNotNeeded: "行内编辑不需要 RAG",
noActiveFile: "没有活动文件",
noActiveEditor: "没有活动编辑器",
noTextSelected: "未选择文本",
noActiveContext: "没有活动文件、编辑器或选区",
invalidChatModel: "无效的聊天模型",
emptyLLMResponse: "LLM 返回空响应",
invalidActiveFile: "无效的活动文件",
readFileError: "读取文件失败:",
applyChangesError: "应用更改失败",
inlineEditError: "行内编辑出错:",
},
prompt: {
"title": "模型提示词设置",
"description": "点击 + 创建新模式",
@ -28,5 +130,16 @@ export default {
"overrideWarning": "。这是一个非常高级的功能,将覆盖所有内置提示,包括工具使用,请谨慎使用",
"previewSystemPrompt": "预览系统提示",
"save": "保存"
},
command: {
"createQuickCommand": "创建快捷命令",
"name": "名称",
"content": "内容",
"createCommand": "创建命令",
"searchPlaceholder": "搜索命令...",
"noCommandsFound": "未找到命令",
"updateCommand": "更新命令",
"errorContentRequired": "请输入模板内容",
"errorNameRequired": "请输入模板名称"
}
};