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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { FileSearch } from 'lucide-react'
import React from 'react' import React from 'react'
import { useApp } from "../../../contexts/AppContext" import { useApp } from "../../../contexts/AppContext"
import { t } from '../../../lang/helpers'
import { ApplyStatus, RegexSearchFilesToolArgs } from "../../../types/apply" import { ApplyStatus, RegexSearchFilesToolArgs } from "../../../types/apply"
import { openMarkdownFile } from "../../../utils/obsidian" 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'}>
<div className={'infio-chat-code-block-header-filename'}> <div className={'infio-chat-code-block-header-filename'}>
<FileSearch size={14} className="infio-chat-code-block-header-icon" /> <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> </div>
</div> </div>

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { FileSearch } from 'lucide-react'
import React from 'react' import React from 'react'
import { useApp } from "../../../contexts/AppContext" import { useApp } from "../../../contexts/AppContext"
import { t } from '../../../lang/helpers'
import { ApplyStatus, SemanticSearchFilesToolArgs } from "../../../types/apply" import { ApplyStatus, SemanticSearchFilesToolArgs } from "../../../types/apply"
import { openMarkdownFile } from "../../../utils/obsidian" 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'}>
<div className={'infio-chat-code-block-header-filename'}> <div className={'infio-chat-code-block-header-filename'}>
<FileSearch size={14} className="infio-chat-code-block-header-icon" /> <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> </div>
</div> </div>

View File

@ -2,6 +2,7 @@ import { Check, Loader2, Settings2, X } from 'lucide-react'
import { PropsWithChildren, useState } from 'react' import { PropsWithChildren, useState } from 'react'
import { useDarkModeContext } from "../../../contexts/DarkModeContext" import { useDarkModeContext } from "../../../contexts/DarkModeContext"
import { t } from '../../../lang/helpers'
import { ApplyStatus, ToolArgs } from "../../../types/apply" import { ApplyStatus, ToolArgs } from "../../../types/apply"
import { MemoizedSyntaxHighlighterWrapper } from "./SyntaxHighlighterWrapper" 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'}>
<div className={'infio-chat-code-block-header-filename'}> <div className={'infio-chat-code-block-header-filename'}>
<Settings2 size={10} className="infio-chat-code-block-header-icon" /> <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>
<div className={'infio-chat-code-block-header-button'}> <div className={'infio-chat-code-block-header-button'}>
<button <button
@ -51,18 +52,18 @@ export default function MarkdownSwitchModeBlock({
{applyStatus === ApplyStatus.Idle ? ( {applyStatus === ApplyStatus.Idle ? (
applying ? ( applying ? (
<> <>
<Loader2 className="spinner" size={14} /> Allowing... <Loader2 className="spinner" size={14} /> {t('chat.reactMarkdown.allowing')}
</> </>
) : ( ) : (
'Allow' t('chat.reactMarkdown.allow')
) )
) : applyStatus === ApplyStatus.Applied ? ( ) : 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> </button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,106 @@
export default { 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: { prompt: {
"title": "Prompts", "title": "Prompts",
"description": "Click + to create a new mode", "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", "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", "previewSystemPrompt": "Preview System Prompt",
"save": "Save" "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 { 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: { prompt: {
"title": "模型提示词设置", "title": "模型提示词设置",
"description": "点击 + 创建新模式", "description": "点击 + 创建新模式",
@ -28,5 +130,16 @@ export default {
"overrideWarning": "。这是一个非常高级的功能,将覆盖所有内置提示,包括工具使用,请谨慎使用", "overrideWarning": "。这是一个非常高级的功能,将覆盖所有内置提示,包括工具使用,请谨慎使用",
"previewSystemPrompt": "预览系统提示", "previewSystemPrompt": "预览系统提示",
"save": "保存" "save": "保存"
},
command: {
"createQuickCommand": "创建快捷命令",
"name": "名称",
"content": "内容",
"createCommand": "创建命令",
"searchPlaceholder": "搜索命令...",
"noCommandsFound": "未找到命令",
"updateCommand": "更新命令",
"errorContentRequired": "请输入模板内容",
"errorNameRequired": "请输入模板名称"
} }
}; };