project-nomad/admin/app/controllers/chats_controller.ts
Claude 2d285bfbc7
feat: add 4 content categories, chat export, and update roadmap
Content Library:
- Children & Family: African Storybooks, Gutenberg children's lit,
  Wikipedia for Schools, PhET simulations, TED-Ed series
- Languages & Reference: Wiktionary in 6 languages (EN/ES/FR/AR/DE/PT)
  across 3 tiers by language coverage
- History & Culture: Project Gutenberg history/biography, History Q&A,
  American history texts, Wikipedia History & Culture
- Legal & Civic: Civics guides, Gutenberg law texts, Law Q&A,
  Personal Finance Q&A, Wikipedia Politics & Government

Chat Export:
- New GET /api/chat/sessions/:id/export endpoint
- Returns full conversation as a downloadable Markdown file
- Filename derived from session title (slugified)
- Includes model name, export timestamp, and all messages

Roadmap:
- Update CATEGORIES-TODO.md to mark 10 categories complete
- Add Trades & Vocational and Communications as next high-priority items

https://claude.ai/code/session_01WfRC4tDeYprykhMrg4PxX6
2026-03-22 21:27:36 +00:00

148 lines
4.7 KiB
TypeScript

import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import { ChatService } from '#services/chat_service'
import { createSessionSchema, updateSessionSchema, addMessageSchema } from '#validators/chat'
import KVStore from '#models/kv_store'
import { SystemService } from '#services/system_service'
import { SERVICE_NAMES } from '../../constants/service_names.js'
@inject()
export default class ChatsController {
constructor(private chatService: ChatService, private systemService: SystemService) {}
async inertia({ inertia, response }: HttpContext) {
const aiAssistantInstalled = await this.systemService.checkServiceInstalled(SERVICE_NAMES.OLLAMA)
if (!aiAssistantInstalled) {
return response.status(404).json({ error: 'AI Assistant service not installed' })
}
const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled')
return inertia.render('chat', {
settings: {
chatSuggestionsEnabled: chatSuggestionsEnabled ?? false,
},
})
}
async index({}: HttpContext) {
return await this.chatService.getAllSessions()
}
async show({ params, response }: HttpContext) {
const sessionId = parseInt(params.id)
const session = await this.chatService.getSession(sessionId)
if (!session) {
return response.status(404).json({ error: 'Session not found' })
}
return session
}
async store({ request, response }: HttpContext) {
try {
const data = await request.validateUsing(createSessionSchema)
const session = await this.chatService.createSession(data.title, data.model)
return response.status(201).json(session)
} catch (error) {
return response.status(500).json({
error: error instanceof Error ? error.message : 'Failed to create session',
})
}
}
async suggestions({ response }: HttpContext) {
try {
const suggestions = await this.chatService.getChatSuggestions()
return response.status(200).json({ suggestions })
} catch (error) {
return response.status(500).json({
error: error instanceof Error ? error.message : 'Failed to get suggestions',
})
}
}
async update({ params, request, response }: HttpContext) {
try {
const sessionId = parseInt(params.id)
const data = await request.validateUsing(updateSessionSchema)
const session = await this.chatService.updateSession(sessionId, data)
return session
} catch (error) {
return response.status(500).json({
error: error instanceof Error ? error.message : 'Failed to update session',
})
}
}
async destroy({ params, response }: HttpContext) {
try {
const sessionId = parseInt(params.id)
await this.chatService.deleteSession(sessionId)
return response.status(204)
} catch (error) {
return response.status(500).json({
error: error instanceof Error ? error.message : 'Failed to delete session',
})
}
}
async addMessage({ params, request, response }: HttpContext) {
try {
const sessionId = parseInt(params.id)
const data = await request.validateUsing(addMessageSchema)
const message = await this.chatService.addMessage(sessionId, data.role, data.content)
return response.status(201).json(message)
} catch (error) {
return response.status(500).json({
error: error instanceof Error ? error.message : 'Failed to add message',
})
}
}
async exportMarkdown({ params, response }: HttpContext) {
const sessionId = parseInt(params.id)
const session = await this.chatService.getSession(sessionId)
if (!session) {
return response.status(404).json({ error: 'Session not found' })
}
const lines: string[] = [
`# ${session.title}`,
``,
`**Model:** ${session.model} `,
`**Exported:** ${new Date().toUTCString()}`,
``,
`---`,
``,
]
for (const msg of session.messages ?? []) {
const label = msg.role === 'user' ? '**You**' : '**Assistant**'
lines.push(`${label}`, ``, msg.content, ``, `---`, ``)
}
const filename = session.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
const markdown = lines.join('\n')
response.header('Content-Type', 'text/markdown; charset=utf-8')
response.header('Content-Disposition', `attachment; filename="${filename}.md"`)
return response.send(markdown)
}
async destroyAll({ response }: HttpContext) {
try {
const result = await this.chatService.deleteAllSessions()
return response.status(200).json(result)
} catch (error) {
return response.status(500).json({
error: error instanceof Error ? error.message : 'Failed to delete all sessions',
})
}
}
}