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
7 changes: 7 additions & 0 deletions core/src/types/message/messageInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ export interface MessageInterface {
*/
listMessages(threadId: string): Promise<ThreadMessage[]>

/**
* Updates an existing message in a thread.
* @param {ThreadMessage} message - The message to be updated (must have existing ID).
* @returns {Promise<ThreadMessage>} A promise that resolves to the updated message.
*/
modifyMessage(message: ThreadMessage): Promise<ThreadMessage>

/**
* Deletes a specific message from a thread.
* @param {string} threadId - The ID of the thread from which the message will be deleted.
Expand Down
100 changes: 92 additions & 8 deletions extensions/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -342,41 +342,73 @@ __metadata:

"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f15485&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=16006d&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/257621cb56db31a4dd3a2b509ec4c61217022e74bbd39cf6a1a172073654b9a65eee94ef9c1b4d4f5d2231d159c8818cb02846f3d88fe14f102f43169ad3737c
peerDependencies:
react: 19.0.0
checksum: 10c0/58a966e7f4aabfe7ee6c34958ca4b90c0de1d1210be9397e8f3fa77dbb4d744c4bc9814647652abd747cdbce630a0035be5e1eb748e38c354199110a5f3dc42f
languageName: node
linkType: hard

"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f15485&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=16006d&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/257621cb56db31a4dd3a2b509ec4c61217022e74bbd39cf6a1a172073654b9a65eee94ef9c1b4d4f5d2231d159c8818cb02846f3d88fe14f102f43169ad3737c
peerDependencies:
react: 19.0.0
checksum: 10c0/58a966e7f4aabfe7ee6c34958ca4b90c0de1d1210be9397e8f3fa77dbb4d744c4bc9814647652abd747cdbce630a0035be5e1eb748e38c354199110a5f3dc42f
languageName: node
linkType: hard

"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f15485&locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=16006d&locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/257621cb56db31a4dd3a2b509ec4c61217022e74bbd39cf6a1a172073654b9a65eee94ef9c1b4d4f5d2231d159c8818cb02846f3d88fe14f102f43169ad3737c
peerDependencies:
react: 19.0.0
checksum: 10c0/58a966e7f4aabfe7ee6c34958ca4b90c0de1d1210be9397e8f3fa77dbb4d744c4bc9814647652abd747cdbce630a0035be5e1eb748e38c354199110a5f3dc42f
languageName: node
linkType: hard

"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f15485&locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=16006d&locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
peerDependencies:
react: 19.0.0
checksum: 10c0/58a966e7f4aabfe7ee6c34958ca4b90c0de1d1210be9397e8f3fa77dbb4d744c4bc9814647652abd747cdbce630a0035be5e1eb748e38c354199110a5f3dc42f
languageName: node
linkType: hard

"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Frag-extension%40workspace%3Arag-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=16006d&locator=%40janhq%2Frag-extension%40workspace%3Arag-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
peerDependencies:
react: 19.0.0
checksum: 10c0/58a966e7f4aabfe7ee6c34958ca4b90c0de1d1210be9397e8f3fa77dbb4d744c4bc9814647652abd747cdbce630a0035be5e1eb748e38c354199110a5f3dc42f
languageName: node
linkType: hard

"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fvector-db-extension%40workspace%3Avector-db-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=16006d&locator=%40janhq%2Fvector-db-extension%40workspace%3Avector-db-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/257621cb56db31a4dd3a2b509ec4c61217022e74bbd39cf6a1a172073654b9a65eee94ef9c1b4d4f5d2231d159c8818cb02846f3d88fe14f102f43169ad3737c
peerDependencies:
react: 19.0.0
checksum: 10c0/58a966e7f4aabfe7ee6c34958ca4b90c0de1d1210be9397e8f3fa77dbb4d744c4bc9814647652abd747cdbce630a0035be5e1eb748e38c354199110a5f3dc42f
languageName: node
linkType: hard

Expand Down Expand Up @@ -418,6 +450,20 @@ __metadata:
languageName: unknown
linkType: soft

"@janhq/rag-extension@workspace:rag-extension":
version: 0.0.0-use.local
resolution: "@janhq/rag-extension@workspace:rag-extension"
dependencies:
"@janhq/core": ../../core/package.tgz
"@janhq/tauri-plugin-rag-api": "link:../../src-tauri/plugins/tauri-plugin-rag"
"@janhq/tauri-plugin-vector-db-api": "link:../../src-tauri/plugins/tauri-plugin-vector-db"
cpx: "npm:1.5.0"
rimraf: "npm:6.0.1"
rolldown: "npm:1.0.0-beta.1"
typescript: "npm:5.9.2"
languageName: unknown
linkType: soft

"@janhq/tauri-plugin-hardware-api@link:../../src-tauri/plugins/tauri-plugin-hardware::locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension":
version: 0.0.0-use.local
resolution: "@janhq/tauri-plugin-hardware-api@link:../../src-tauri/plugins/tauri-plugin-hardware::locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension"
Expand All @@ -430,6 +476,44 @@ __metadata:
languageName: node
linkType: soft

"@janhq/tauri-plugin-rag-api@link:../../src-tauri/plugins/tauri-plugin-rag::locator=%40janhq%2Frag-extension%40workspace%3Arag-extension":
version: 0.0.0-use.local
resolution: "@janhq/tauri-plugin-rag-api@link:../../src-tauri/plugins/tauri-plugin-rag::locator=%40janhq%2Frag-extension%40workspace%3Arag-extension"
languageName: node
linkType: soft

"@janhq/tauri-plugin-rag-api@link:../../src-tauri/plugins/tauri-plugin-rag::locator=%40janhq%2Fvector-db-extension%40workspace%3Avector-db-extension":
version: 0.0.0-use.local
resolution: "@janhq/tauri-plugin-rag-api@link:../../src-tauri/plugins/tauri-plugin-rag::locator=%40janhq%2Fvector-db-extension%40workspace%3Avector-db-extension"
languageName: node
linkType: soft

"@janhq/tauri-plugin-vector-db-api@link:../../src-tauri/plugins/tauri-plugin-vector-db::locator=%40janhq%2Frag-extension%40workspace%3Arag-extension":
version: 0.0.0-use.local
resolution: "@janhq/tauri-plugin-vector-db-api@link:../../src-tauri/plugins/tauri-plugin-vector-db::locator=%40janhq%2Frag-extension%40workspace%3Arag-extension"
languageName: node
linkType: soft

"@janhq/tauri-plugin-vector-db-api@link:../../src-tauri/plugins/tauri-plugin-vector-db::locator=%40janhq%2Fvector-db-extension%40workspace%3Avector-db-extension":
version: 0.0.0-use.local
resolution: "@janhq/tauri-plugin-vector-db-api@link:../../src-tauri/plugins/tauri-plugin-vector-db::locator=%40janhq%2Fvector-db-extension%40workspace%3Avector-db-extension"
languageName: node
linkType: soft

"@janhq/vector-db-extension@workspace:vector-db-extension":
version: 0.0.0-use.local
resolution: "@janhq/vector-db-extension@workspace:vector-db-extension"
dependencies:
"@janhq/core": ../../core/package.tgz
"@janhq/tauri-plugin-rag-api": "link:../../src-tauri/plugins/tauri-plugin-rag"
"@janhq/tauri-plugin-vector-db-api": "link:../../src-tauri/plugins/tauri-plugin-vector-db"
cpx: "npm:1.5.0"
rimraf: "npm:6.0.1"
rolldown: "npm:1.0.0-beta.1"
typescript: "npm:5.9.2"
languageName: unknown
linkType: soft

"@jridgewell/sourcemap-codec@npm:^1.5.0":
version: 1.5.0
resolution: "@jridgewell/sourcemap-codec@npm:1.5.0"
Expand Down
24 changes: 24 additions & 0 deletions src-tauri/plugins/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,30 @@ __metadata:
languageName: unknown
linkType: soft

"@janhq/tauri-plugin-rag-api@workspace:tauri-plugin-rag":
version: 0.0.0-use.local
resolution: "@janhq/tauri-plugin-rag-api@workspace:tauri-plugin-rag"
dependencies:
"@rollup/plugin-typescript": "npm:^12.0.0"
"@tauri-apps/api": "npm:>=2.0.0-beta.6"
rollup: "npm:^4.9.6"
tslib: "npm:^2.6.2"
typescript: "npm:^5.3.3"
languageName: unknown
linkType: soft

"@janhq/tauri-plugin-vector-db-api@workspace:tauri-plugin-vector-db":
version: 0.0.0-use.local
resolution: "@janhq/tauri-plugin-vector-db-api@workspace:tauri-plugin-vector-db"
dependencies:
"@rollup/plugin-typescript": "npm:^12.0.0"
"@tauri-apps/api": "npm:>=2.0.0-beta.6"
rollup: "npm:^4.9.6"
tslib: "npm:^2.6.2"
typescript: "npm:^5.3.3"
languageName: unknown
linkType: soft

"@npmcli/agent@npm:^3.0.0":
version: 3.0.0
resolution: "@npmcli/agent@npm:3.0.0"
Expand Down
61 changes: 59 additions & 2 deletions web-app/src/containers/GenerateResponseButton.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,72 @@
import { useChat } from '@/hooks/useChat'
import { useMessages } from '@/hooks/useMessages'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { Play } from 'lucide-react'
import { useShallow } from 'zustand/react/shallow'
import { useMemo } from 'react'
import { MessageStatus } from '@janhq/core'

Check warning on line 7 in web-app/src/containers/GenerateResponseButton.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

1-7 lines are not covered with tests

export const GenerateResponseButton = ({ threadId }: { threadId: string }) => {
export const GenerateResponseButton = ({
threadId,
isModelMismatch = false
}: {

Check warning on line 12 in web-app/src/containers/GenerateResponseButton.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

9-12 lines are not covered with tests
threadId: string
isModelMismatch?: boolean
}) => {
const { t } = useTranslation()
const deleteMessage = useMessages((state) => state.deleteMessage)
const { messages } = useMessages(
useShallow((state) => ({
messages: state.messages[threadId],
}))
)
const sendMessage = useChat()

Check warning on line 23 in web-app/src/containers/GenerateResponseButton.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

15-23 lines are not covered with tests

// Detect if last message is a partial assistant response (user stopped midway)
const isPartialResponse = useMemo(() => {
if (!messages || messages.length < 2) return false
const lastMessage = messages[messages.length - 1]
const secondLastMessage = messages[messages.length - 2]

Check warning on line 29 in web-app/src/containers/GenerateResponseButton.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

26-29 lines are not covered with tests

return (
lastMessage?.role === 'assistant' &&
lastMessage?.status === MessageStatus.Stopped &&
secondLastMessage?.role === 'user' &&
!lastMessage?.metadata?.tool_calls

Check warning on line 35 in web-app/src/containers/GenerateResponseButton.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

31-35 lines are not covered with tests
)
}, [messages])

Check warning on line 37 in web-app/src/containers/GenerateResponseButton.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

37 line is not covered with tests

const generateAIResponse = () => {

Check warning on line 39 in web-app/src/containers/GenerateResponseButton.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

39 line is not covered with tests
// If model mismatch, delete the partial message and regenerate from scratch
if (isPartialResponse && isModelMismatch) {
const partialMessage = messages[messages.length - 1]
const userMessage = messages[messages.length - 2]

Check warning on line 43 in web-app/src/containers/GenerateResponseButton.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

41-43 lines are not covered with tests
// Delete the partial message from the old model
deleteMessage(partialMessage.thread_id, partialMessage.id ?? '')

Check warning on line 45 in web-app/src/containers/GenerateResponseButton.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

45 line is not covered with tests
// Send a new message with the new model
if (userMessage?.content?.[0]?.text?.value) {
sendMessage(userMessage.content[0].text.value, false)
}
return
}

Check warning on line 51 in web-app/src/containers/GenerateResponseButton.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

47-51 lines are not covered with tests

// If partial response from the same model, continue from where it stopped
if (isPartialResponse && !isModelMismatch) {
const partialMessage = messages[messages.length - 1]
const userMessage = messages[messages.length - 2]
if (userMessage?.content?.[0]?.text?.value) {
sendMessage(
userMessage.content[0].text.value,
false,
undefined,
undefined,
undefined,
partialMessage.id
)
}
return
}

const latestUserMessage = messages[messages.length - 1]
if (
latestUserMessage?.content?.[0]?.text?.value &&
Expand All @@ -39,7 +92,11 @@
className="mx-2 bg-main-view-fg/10 px-2 border border-main-view-fg/5 flex items-center justify-center rounded-xl gap-x-2 cursor-pointer pointer-events-auto"
onClick={generateAIResponse}
>
<p className="text-xs">{t('common:generateAiResponse')}</p>
<p className="text-xs">
{isPartialResponse && !isModelMismatch
? t('common:continueAiResponse')
: t('common:generateAiResponse')}
</p>
<Play size={12} />
</div>
)
Expand Down
47 changes: 42 additions & 5 deletions web-app/src/containers/ScrollToBottom.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useThreadScrolling } from '@/hooks/useThreadScrolling'
import { memo } from 'react'
import { memo, useEffect } from 'react'
import { GenerateResponseButton } from './GenerateResponseButton'
import { useMessages } from '@/hooks/useMessages'
import { useShallow } from 'zustand/react/shallow'
Expand All @@ -8,6 +8,8 @@ import { cn } from '@/lib/utils'
import { ArrowDown } from 'lucide-react'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { useAppState } from '@/hooks/useAppState'
import { MessageStatus } from '@janhq/core'
import { useModelProvider } from '@/hooks/useModelProvider'

const ScrollToBottom = ({
threadId,
Expand All @@ -27,12 +29,47 @@ const ScrollToBottom = ({
)

const streamingContent = useAppState((state) => state.streamingContent)
const selectedModel = useModelProvider((state) => state.selectedModel)
const updateMessage = useMessages((state) => state.updateMessage)

// Check if last message is a partial assistant response and show continue button (user interrupted)
const isPartialResponse =
messages.length >= 2 &&
messages[messages.length - 1]?.role === 'assistant' &&
messages[messages.length - 1]?.status === MessageStatus.Stopped &&
messages[messages.length - 2]?.role === 'user' &&
!messages[messages.length - 1]?.metadata?.tool_calls

// Check if the partial response was generated by a different model
const partialMessage = messages[messages.length - 1]
const partialMessageModelId = partialMessage?.metadata?.modelId as string | undefined
const hasModelSwitchedFlag = partialMessage?.metadata?.modelSwitched === true

const currentModelMismatch = isPartialResponse &&
partialMessageModelId !== undefined &&
partialMessageModelId !== selectedModel?.id

const isModelMismatch = isPartialResponse && (currentModelMismatch || hasModelSwitchedFlag)

useEffect(() => {
if (currentModelMismatch && !hasModelSwitchedFlag && partialMessage) {
updateMessage({
...partialMessage,
metadata: {
...partialMessage.metadata,
modelSwitched: true,
},
})
}
}, [currentModelMismatch, hasModelSwitchedFlag, partialMessage, updateMessage])

const showGenerateAIResponseBtn =
(messages[messages.length - 1]?.role === 'user' ||
((messages[messages.length - 1]?.role === 'user' ||
(messages[messages.length - 1]?.metadata &&
'tool_calls' in (messages[messages.length - 1].metadata ?? {}))) &&
!streamingContent
'tool_calls' in (messages[messages.length - 1].metadata ?? {})) ||
isPartialResponse ||
isModelMismatch) &&
!streamingContent)

return (
<div
Expand All @@ -57,7 +94,7 @@ const ScrollToBottom = ({
</div>
)}
{showGenerateAIResponseBtn && (
<GenerateResponseButton threadId={threadId} />
<GenerateResponseButton threadId={threadId} isModelMismatch={isModelMismatch} />
)}
</div>
)
Expand Down
10 changes: 9 additions & 1 deletion web-app/src/containers/StreamingContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useAppState } from '@/hooks/useAppState'
import { ThreadContent } from './ThreadContent'
import { memo, useMemo } from 'react'
import { useMessages } from '@/hooks/useMessages'
import { MessageStatus } from '@janhq/core'

type Props = {
threadId: string
Expand Down Expand Up @@ -48,12 +49,19 @@ export const StreamingContent = memo(({ threadId }: Props) => {
return extractReasoningSegment(text)
}, [lastAssistant])

if (!streamingContent || streamingContent.thread_id !== threadId) return null
if (!streamingContent || streamingContent.thread_id !== threadId) {
return null
}

if (streamingReasoning && streamingReasoning === lastAssistantReasoning) {
return null
}

// Don't show streaming content if there's already a stopped message
if (lastAssistant?.status === MessageStatus.Stopped) {
return null
}

// Pass a new object to ThreadContent to avoid reference issues
// The streaming content is always the last message
return (
Expand Down
Loading
Loading