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);
}