Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/presenter/floatingButtonPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class FloatingButtonPresenter {
try {
// 触发内置事件处理器
handleShowHiddenWindow(true)
} catch (error) {
} catch {
}
})

Expand Down
158 changes: 90 additions & 68 deletions src/renderer/src/components/ChatInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@

<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { computed, onMounted, ref, watch } from 'vue'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import {
Expand Down Expand Up @@ -219,6 +219,20 @@ const mcpStore = useMcpStore()
const { toast } = useToast()
const { t } = useI18n()
searchHistory.resetIndex()

// 历史记录placeholder相关变量需要在editor初始化之前定义
const currentHistoryPlaceholder = ref('')
const showHistoryPlaceholder = ref(false)

// 计算动态placeholder
const dynamicPlaceholder = computed(() => {
if (currentHistoryPlaceholder.value) {
// 当有历史记录时,只显示历史记录内容和提示
return `${currentHistoryPlaceholder.value} ${t('chat.input.historyPlaceholder')}`
}
return t('chat.input.placeholder')
})

const editor = new Editor({
editorProps: {
attributes: {
Expand All @@ -241,8 +255,7 @@ const editor = new Editor({
}),
Placeholder.configure({
placeholder: () => {
const placeholder = t('chat.input.placeholder')
return `${placeholder}`
return dynamicPlaceholder.value
}
}),
HardBreak.extend({
Expand Down Expand Up @@ -295,6 +308,11 @@ const editor = new Editor({
],
onUpdate: ({ editor }) => {
inputText.value = editor.getText()

// 如果用户开始输入且有历史记录placeholder,清除它
if (inputText.value.trim() && currentHistoryPlaceholder.value) {
clearHistoryPlaceholder()
}
}
})

Expand Down Expand Up @@ -567,6 +585,9 @@ const emitSend = async () => {
emit('send', messageContent)
inputText.value = ''
editor.chain().clearContent().blur().run()

// 清除历史记录placeholder
clearHistoryPlaceholder()

// 清理已上传的文件
if (selectedFiles.value.length > 0) {
Expand Down Expand Up @@ -687,7 +708,8 @@ const handleEditorEnter = (e: KeyboardEvent) => {
}

// Only handle enter if there's no active suggestion popup
if (editor.isActive('mention') || document.querySelector('.tippy-box')) {
const hasMentionSuggestion = editor.isActive('mention') || document.querySelector('.tippy-box')
if (hasMentionSuggestion) {
// Don't prevent default - let the mention suggestion handle it
return
}
Expand Down Expand Up @@ -919,82 +941,82 @@ watch(
}
)

// 监听 dynamicPlaceholder 变化并更新编辑器
watch(
dynamicPlaceholder,
() => {
// 强制更新 TipTap 的 placeholder 显示
updatePlaceholder()
}
)

// 处理历史记录placeholder
const setHistoryPlaceholder = (text: string) => {
currentHistoryPlaceholder.value = text
showHistoryPlaceholder.value = true

// 强制更新 TipTap 的 placeholder
updatePlaceholder()
}

const clearHistoryPlaceholder = () => {
currentHistoryPlaceholder.value = ''
showHistoryPlaceholder.value = false

// 强制更新 TipTap 的 placeholder
updatePlaceholder()

// 重置搜索历史索引
searchHistory.resetIndex()
}

// 强制更新 TipTap 编辑器的 placeholder
const updatePlaceholder = () => {
// 使用 nextTick 确保 Vue 的响应式更新完成后再更新编辑器
nextTick(() => {
// 强制重新渲染编辑器视图
const { state } = editor
editor.view.updateState(state)
})
}

function onKeydown(e: KeyboardEvent) {
if (e.code === 'Enter' && !e.shiftKey) {
// 阻止默认行为,避免换行
handleEditorEnter(e)
e.preventDefault()
}

if (e.code === 'ArrowUp') {
const contentEditableDiv = e.target as HTMLDivElement
if (isCursorInFirstLine(contentEditableDiv)) {
const currentContent = editor.getText().trim()

// 如果当前有内容,先将其插入到搜索历史的当前位置
if (currentContent) {
searchHistory.insertAtCurrent(currentContent)
}

const previousSearch = searchHistory.getPrevious()
if (previousSearch !== null) {
editor.commands.setContent(previousSearch)
}
e.preventDefault()
// 历史记录功能:只在输入框为空时生效
const currentContent = editor.getText().trim()

if (e.code === 'ArrowUp' && !currentContent) {
const previousSearch = searchHistory.getPrevious()
if (previousSearch !== null) {
setHistoryPlaceholder(previousSearch)
}
} else if (e.code === 'ArrowDown') {
const contentEditableDiv = e.target as HTMLDivElement
if (isCursorInLastLine(contentEditableDiv)) {
const currentContent = editor.getText().trim()

// 如果当前有内容,先将其插入到搜索历史的当前位置
if (currentContent) {
searchHistory.insertAtCurrent(currentContent)
}

const nextSearch = searchHistory.getNext()
if (nextSearch !== null) {
editor.commands.setContent(nextSearch)
}
e.preventDefault()
e.preventDefault()
} else if (e.code === 'ArrowDown' && !currentContent) {
const nextSearch = searchHistory.getNext()
if (nextSearch !== null) {
setHistoryPlaceholder(nextSearch)
}
e.preventDefault()
} else if (e.code === 'Tab' && currentHistoryPlaceholder.value) {
// Tab 键确认填充历史记录
e.preventDefault()
editor.commands.setContent(currentHistoryPlaceholder.value)
clearHistoryPlaceholder()
} else if (e.code === 'Escape' && currentHistoryPlaceholder.value) {
// Escape 键取消历史记录placeholder
e.preventDefault()
clearHistoryPlaceholder()
} else if (currentHistoryPlaceholder.value && e.key.length === 1) {
// 如果有历史记录placeholder且用户开始输入,清除placeholder
clearHistoryPlaceholder()
}
}

function isCursorInFirstLine(contentEditableDiv: HTMLDivElement): boolean {
const selection = window.getSelection()
if (!selection || !selection.rangeCount) return false

const range = selection.getRangeAt(0)
const startContainer = range.startContainer
const parentElement =
startContainer.nodeType === Node.TEXT_NODE
? startContainer.parentElement
: (startContainer as HTMLElement)

if (!parentElement) return false

const firstLineElement = contentEditableDiv.firstChild
return parentElement === firstLineElement || contentEditableDiv.contains(firstLineElement)
}

function isCursorInLastLine(contentEditableDiv: HTMLDivElement): boolean {
const selection = window.getSelection()
if (!selection || !selection.rangeCount) return false

const range = selection.getRangeAt(0)
const endContainer = range.endContainer
const parentElement =
endContainer.nodeType === Node.TEXT_NODE
? endContainer.parentElement
: (endContainer as HTMLElement)

if (!parentElement) return false

const lastLineElement = contentEditableDiv.lastChild
return parentElement === lastLineElement || contentEditableDiv.contains(lastLineElement)
}

defineExpose({
setText: (text: string) => {
inputText.value = text
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/en-US/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"promptFilesAdded": "Prompt Files Added",
"promptFilesAddedDesc": "Successfully added {count} files",
"promptFilesError": "File Processing Error",
"promptFilesErrorDesc": "{count} files failed to process"
"promptFilesErrorDesc": "{count} files failed to process",
"historyPlaceholder": "(Press Tab to fill)"
},
"features": {
"deepThinking": "Deep Thinking",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/fa-IR/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"promptFilesAdded": "پرونده‌ها افزوده شد",
"promptFilesAddedDesc": "{count} پرونده با موفقیت افزوده شد",
"promptFilesError": "خطا در پردازش پرونده",
"promptFilesErrorDesc": "پردازش {count} پرونده ناموفق بود"
"promptFilesErrorDesc": "پردازش {count} پرونده ناموفق بود",
"historyPlaceholder": "(برای پر کردن برگه را فشار دهید)"
},
"features": {
"deepThinking": "تفکر عمیق",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/fr-FR/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"promptFilesAdded": "Fichiers de prompt ajoutés",
"promptFilesAddedDesc": "{count} fichiers ajoutés avec succès",
"promptFilesError": "Erreur de traitement des fichiers",
"promptFilesErrorDesc": "Échec du traitement de {count} fichiers"
"promptFilesErrorDesc": "Échec du traitement de {count} fichiers",
"historyPlaceholder": "(Appuyez sur l'onglet pour remplir)"
},
"features": {
"deepThinking": "Réflexion approfondie",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/ja-JP/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"promptFilesAdded": "プロンプトファイルが追加されました",
"promptFilesAddedDesc": "{count}個のファイルが正常に追加されました",
"promptFilesError": "ファイル処理エラー",
"promptFilesErrorDesc": "{count}個のファイルの処理に失敗しました"
"promptFilesErrorDesc": "{count}個のファイルの処理に失敗しました",
"historyPlaceholder": "(タブを押して入力)"
},
"features": {
"deepThinking": "深い思考",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/ko-KR/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"promptFilesAdded": "프롬프트 파일이 추가됨",
"promptFilesAddedDesc": "{count}개 파일이 성공적으로 추가되었습니다",
"promptFilesError": "파일 처리 오류",
"promptFilesErrorDesc": "{count}개 파일 처리에 실패했습니다"
"promptFilesErrorDesc": "{count}개 파일 처리에 실패했습니다",
"historyPlaceholder": "(채우려면 탭을 누릅니다)"
},
"features": {
"deepThinking": "심층 사고",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/ru-RU/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"promptFilesAdded": "Файлы промпта добавлены",
"promptFilesAddedDesc": "Успешно добавлено {count} файлов",
"promptFilesError": "Ошибка обработки файлов",
"promptFilesErrorDesc": "Не удалось обработать {count} файлов"
"promptFilesErrorDesc": "Не удалось обработать {count} файлов",
"historyPlaceholder": "(Нажмите вкладку, чтобы заполнить)"
},
"features": {
"deepThinking": "Глубокое мышление",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/zh-CN/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"promptFilesAdded": "Prompt 文件已添加",
"promptFilesAddedDesc": "已成功添加 {count} 个文件",
"promptFilesError": "文件处理错误",
"promptFilesErrorDesc": "{count} 个文件处理失败"
"promptFilesErrorDesc": "{count} 个文件处理失败",
"historyPlaceholder": "(按 Tab 填充)"
},
"features": {
"deepThinking": "深度思考",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/zh-HK/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"promptFilesAdded": "Prompt 文件已添加",
"promptFilesAddedDesc": "已成功添加 {count} 個文件",
"promptFilesError": "文件處理錯誤",
"promptFilesErrorDesc": "{count} 個文件處理失敗"
"promptFilesErrorDesc": "{count} 個文件處理失敗",
"historyPlaceholder": "(按 Tab 填充)"
},
"features": {
"deepThinking": "深度思考",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/zh-TW/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"promptFilesAdded": "Prompt 檔案已新增",
"promptFilesAddedDesc": "已成功新增 {count} 個檔案",
"promptFilesError": "檔案處理錯誤",
"promptFilesErrorDesc": "{count} 個檔案處理失敗"
"promptFilesErrorDesc": "{count} 個檔案處理失敗",
"historyPlaceholder": "(按 Tab 填充)"
},
"features": {
"deepThinking": "深度思考",
Expand Down
2 changes: 0 additions & 2 deletions src/renderer/src/lib/searchHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@ export class SearchHistory {
}

addSearch(query: string) {
// 如果 history 已经满了,移除最旧的记录
if (query && query !== this.history[this.history.length - 1]) {
if (this.history.length >= this.maxHistorySize) {
this.history.shift() // Remove the oldest search
}
this.history.push(query)
this.currentIndex = this.history.length // Reset index to the end
}
console.log('Search history updated:', this.history)
}

getPrevious() {
Expand Down