Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions core/src/browser/extensions/engines/AIEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type ToolChoice = 'none' | 'auto' | 'required' | ToolCallSpec
export interface chatCompletionRequest {
model: string // Model ID, though for local it might be implicit via sessionInfo
messages: chatCompletionRequestMessage[]
thread_id?: string // Thread/conversation ID for context tracking
return_progress?: boolean
tools?: Tool[]
tool_choice?: ToolChoice
Expand Down
160 changes: 160 additions & 0 deletions extensions-web/src/conversational-web/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* Conversation API wrapper using JanAuthProvider
*/

import { getSharedAuthService, JanAuthService } from '../shared/auth'
import { CONVERSATION_API_ROUTES } from './const'

Check warning on line 6 in extensions-web/src/conversational-web/api.ts

View workflow job for this annotation

GitHub Actions / coverage-check

5-6 lines are not covered with tests
import {
Conversation,
ConversationResponse,
ListConversationsParams,
ListConversationsResponse,
PaginationParams,
PaginatedResponse,
ConversationItem,
ListConversationItemsParams,
ListConversationItemsResponse
} from './types'

declare const JAN_API_BASE: string

export class RemoteApi {

Check warning on line 21 in extensions-web/src/conversational-web/api.ts

View workflow job for this annotation

GitHub Actions / coverage-check

21 line is not covered with tests
private authService: JanAuthService

constructor() {
this.authService = getSharedAuthService()
}

Check warning on line 26 in extensions-web/src/conversational-web/api.ts

View workflow job for this annotation

GitHub Actions / coverage-check

24-26 lines are not covered with tests

async createConversation(
data: Conversation
): Promise<ConversationResponse> {
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATIONS}`

Check warning on line 31 in extensions-web/src/conversational-web/api.ts

View workflow job for this annotation

GitHub Actions / coverage-check

28-31 lines are not covered with tests

return this.authService.makeAuthenticatedRequest<ConversationResponse>(
url,
{
method: 'POST',
body: JSON.stringify(data),
}
)
}

Check warning on line 40 in extensions-web/src/conversational-web/api.ts

View workflow job for this annotation

GitHub Actions / coverage-check

33-40 lines are not covered with tests

async updateConversation(
conversationId: string,
data: Conversation
): Promise<ConversationResponse> {
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`

Check warning on line 46 in extensions-web/src/conversational-web/api.ts

View workflow job for this annotation

GitHub Actions / coverage-check

42-46 lines are not covered with tests

return this.authService.makeAuthenticatedRequest<ConversationResponse>(
url,
{
method: 'PATCH',
body: JSON.stringify(data),
}
)
}

Check warning on line 55 in extensions-web/src/conversational-web/api.ts

View workflow job for this annotation

GitHub Actions / coverage-check

48-55 lines are not covered with tests

async listConversations(
params?: ListConversationsParams
): Promise<ListConversationsResponse> {
const queryParams = new URLSearchParams()

Check warning on line 60 in extensions-web/src/conversational-web/api.ts

View workflow job for this annotation

GitHub Actions / coverage-check

57-60 lines are not covered with tests

if (params?.limit !== undefined) {
queryParams.append('limit', params.limit.toString())
}
if (params?.after) {
queryParams.append('after', params.after)
}
if (params?.order) {
queryParams.append('order', params.order)
}

Check warning on line 70 in extensions-web/src/conversational-web/api.ts

View workflow job for this annotation

GitHub Actions / coverage-check

62-70 lines are not covered with tests

const queryString = queryParams.toString()
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATIONS}${queryString ? `?${queryString}` : ''}`

Check warning on line 73 in extensions-web/src/conversational-web/api.ts

View workflow job for this annotation

GitHub Actions / coverage-check

72-73 lines are not covered with tests

return this.authService.makeAuthenticatedRequest<ListConversationsResponse>(
url,
{
method: 'GET',
}
)
}

/**
* Generic method to fetch all pages of paginated data
*/
async fetchAllPaginated<T>(
fetchFn: (params: PaginationParams) => Promise<PaginatedResponse<T>>,
initialParams?: Partial<PaginationParams>
): Promise<T[]> {
const allItems: T[] = []
let after: string | undefined = undefined
let hasMore = true
const limit = initialParams?.limit || 100

while (hasMore) {
const response = await fetchFn({
limit,
after,
...initialParams,
})

allItems.push(...response.data)
hasMore = response.has_more
after = response.last_id
}

return allItems
}

async getAllConversations(): Promise<ConversationResponse[]> {
return this.fetchAllPaginated<ConversationResponse>(
(params) => this.listConversations(params)
)
}

async deleteConversation(conversationId: string): Promise<void> {
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`

await this.authService.makeAuthenticatedRequest(
url,
{
method: 'DELETE',
}
)
}

async listConversationItems(
conversationId: string,
params?: Omit<ListConversationItemsParams, 'conversation_id'>
): Promise<ListConversationItemsResponse> {
const queryParams = new URLSearchParams()

if (params?.limit !== undefined) {
queryParams.append('limit', params.limit.toString())
}
if (params?.after) {
queryParams.append('after', params.after)
}
if (params?.order) {
queryParams.append('order', params.order)
}

const queryString = queryParams.toString()
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_ITEMS(conversationId)}${queryString ? `?${queryString}` : ''}`

return this.authService.makeAuthenticatedRequest<ListConversationItemsResponse>(
url,
{
method: 'GET',
}
)
}

async getAllConversationItems(conversationId: string): Promise<ConversationItem[]> {
return this.fetchAllPaginated<ConversationItem>(
(params) => this.listConversationItems(conversationId, params),
{ limit: 100, order: 'asc' }
)
}
}
17 changes: 17 additions & 0 deletions extensions-web/src/conversational-web/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* API Constants for Conversational Web
*/


export const CONVERSATION_API_ROUTES = {
CONVERSATIONS: '/conversations',
CONVERSATION_BY_ID: (id: string) => `/conversations/${id}`,
CONVERSATION_ITEMS: (id: string) => `/conversations/${id}/items`,
} as const

export const DEFAULT_ASSISTANT = {
id: 'jan',
name: 'Jan',
avatar: '👋',
created_at: 1747029866.542,
}
154 changes: 154 additions & 0 deletions extensions-web/src/conversational-web/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/**
* Web Conversational Extension
* Implements thread and message management using IndexedDB
*/

import {
Thread,
ThreadMessage,
ConversationalExtension,
ThreadAssistantInfo,
} from '@janhq/core'
import { RemoteApi } from './api'
import { getDefaultAssistant, ObjectParser, combineConversationItemsToMessages } from './utils'

export default class ConversationalExtensionWeb extends ConversationalExtension {
private remoteApi: RemoteApi | undefined

async onLoad() {
console.log('Loading Web Conversational Extension')
this.remoteApi = new RemoteApi()
}

onUnload() {}

// Thread Management
async listThreads(): Promise<Thread[]> {
try {
if (!this.remoteApi) {
throw new Error('RemoteApi not initialized')
}
const conversations = await this.remoteApi.getAllConversations()
console.log('!!!Listed threads:', conversations.map(ObjectParser.conversationToThread))
return conversations.map(ObjectParser.conversationToThread)
} catch (error) {
console.error('Failed to list threads:', error)
return []
}
}

async createThread(thread: Thread): Promise<Thread> {
try {
if (!this.remoteApi) {
throw new Error('RemoteApi not initialized')
}
const response = await this.remoteApi.createConversation(
ObjectParser.threadToConversation(thread)
)
// Create a new thread object with the server's ID
const createdThread = {
...thread,
id: response.id,
assistants: thread.assistants.map(getDefaultAssistant)
}
console.log('!!!Created thread:', createdThread)
return createdThread
} catch (error) {
console.error('Failed to create thread:', error)
throw error
}
}

async modifyThread(thread: Thread): Promise<void> {
try {
if (!this.remoteApi) {
throw new Error('RemoteApi not initialized')
}
await this.remoteApi.updateConversation(
thread.id,
ObjectParser.threadToConversation(thread)
)
console.log('!!!Modified thread:', thread)
} catch (error) {
console.error('Failed to modify thread:', error)
throw error
}
}

async deleteThread(threadId: string): Promise<void> {
try {
if (!this.remoteApi) {
throw new Error('RemoteApi not initialized')
}
await this.remoteApi.deleteConversation(threadId)
console.log('!!!Deleted thread:', threadId)
} catch (error) {
console.error('Failed to delete thread:', error)
throw error
}
}

// Message Management
async createMessage(message: ThreadMessage): Promise<ThreadMessage> {
console.log('!!!Created message:', message)
return message
}

async listMessages(threadId: string): Promise<ThreadMessage[]> {
try {
if (!this.remoteApi) {
throw new Error('RemoteApi not initialized')
}
console.log('!!!Listing messages for thread:', threadId)

// Fetch all conversation items from the API
const items = await this.remoteApi.getAllConversationItems(threadId)

// Convert and combine conversation items to thread messages
const messages = combineConversationItemsToMessages(items, threadId)

console.log('!!!Fetched messages:', messages)
return messages
} catch (error) {
console.error('Failed to list messages:', error)
return []
}
}

async modifyMessage(message: ThreadMessage): Promise<ThreadMessage> {
console.log('!!!Modified message:', message)
return message
}

async deleteMessage(threadId: string, messageId: string): Promise<void> {
console.log('!!!Deleted message:', threadId, messageId)
}

async getThreadAssistant(threadId: string): Promise<ThreadAssistantInfo> {
console.log('!!!Getting assistant for thread:', threadId)
return { id: 'jan', name: 'Jan', model: { id: 'jan-v1-4b' } }
}

async createThreadAssistant(
threadId: string,
assistant: ThreadAssistantInfo
): Promise<ThreadAssistantInfo> {
console.log('!!!Creating assistant for thread:', threadId, assistant)
return assistant
}

async modifyThreadAssistant(
threadId: string,
assistant: ThreadAssistantInfo
): Promise<ThreadAssistantInfo> {
console.log('!!!Modifying assistant for thread:', threadId, assistant)
return assistant
}

async getThreadAssistantInfo(
threadId: string
): Promise<ThreadAssistantInfo | undefined> {
console.log('!!!Getting assistant info for thread:', threadId)
return { id: 'jan', name: 'Jan', model: { id: 'jan-v1-4b' } }
}
}
Loading
Loading