-
- Privacy Policy
+
+
+ T
+
+ TNA Studio
+ Training Interview Tracker
+
-
+
+
+
+
+
+
Structured interviews, durable insight
+
Turn training conversations into clear workforce priorities.
+
Run consistent training-needs assessment interviews, capture voice-style or typed responses, produce grounded summaries, and retain structured findings for trend analysis across participants.
+
+
+
+
+
Authentication keeps interview records private. Use the admin interface to access the working MVP slice.
+
+
+
+
+
+
+
+
+
+
Live session
+
Question 2 of 5
+
+ Typed mode
+
+
+
Main question
+
Describe training that is most effective for your learning and why.
+
+
+
Captured answer becomes part of the participant summary, finding tags, and future cross-interview pattern analysis.
+
+
+
+
+
+
+
+
+
+
First MVP workflow
+
A thin slice from interview start to analysis-ready record.
+
+
+ {workflowCards.map((card, index) => (
+
+
{index + 1}
+
{card.title}
+
{card.text}
+
+ ))}
+
+
+
+
+
+
+
5
+
Core questions kept consistent across interviews.
+
+
+
≤60m
+
Designed to keep facilitation moving without becoming a long survey.
+
+
+
Trends
+
Themes, recurring gaps, delivery preferences, and communication channels accumulate over time.
+
+
+
+
+
- );
+ )
}
Starter.getLayout = function getLayout(page: ReactElement) {
- return {page};
-};
-
+ return {page}
+}
diff --git a/frontend/src/pages/interview-workspace.tsx b/frontend/src/pages/interview-workspace.tsx
new file mode 100644
index 0000000..dd598f0
--- /dev/null
+++ b/frontend/src/pages/interview-workspace.tsx
@@ -0,0 +1,369 @@
+import { mdiAccountVoice, mdiChartTimelineVariant, mdiCheckCircleOutline, mdiClipboardTextOutline, mdiMicrophoneOutline, mdiPencilOutline } from '@mdi/js'
+import axios from 'axios'
+import Head from 'next/head'
+import React, { ReactElement, useEffect, useMemo, useState } from 'react'
+import BaseButton from '../components/BaseButton'
+import CardBox from '../components/CardBox'
+import SectionMain from '../components/SectionMain'
+import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
+import LayoutAuthenticated from '../layouts/Authenticated'
+import { getPageTitle } from '../config'
+import { useAppSelector } from '../stores/hooks'
+
+type InterviewMode = 'voice' | 'typed' | 'mixed'
+
+type SavedInterview = {
+ id: string
+ mode?: InterviewMode
+ status?: string
+ final_summary?: string
+ createdAt?: string
+ completed_at?: string
+ participant?: {
+ display_name?: string
+ department?: string
+ job_title?: string
+ }
+ interview_responses_interview?: Array<{
+ id: string
+ turn_number: number
+ response_text: string
+ }>
+}
+
+type Analysis = {
+ interviewCount: number
+ findingCounts: Record
+ themes: Array<{ theme: string; count: number }>
+}
+
+const QUESTIONS = [
+ 'Do you have any professional development training that you recommend that would be of interest or value to your colleagues?',
+ 'Describe training that is most effective for your learning and why.',
+ 'What is a reasonable amount of time that you could dedicate to training?',
+ 'What makes learning memorable for you? How can training be designed to help stick for you?',
+ 'How do you learn about training opportunities? Is that method effective? What could make it more effective?',
+]
+
+const SECTION_TITLES = [
+ 'Participant priorities and interests',
+ 'Preferred learning methods',
+ 'Time available for training',
+ 'Memorable and effective learning',
+ 'Training opportunity channels',
+]
+
+const emptyAnswers = QUESTIONS.map(() => ({ answer: '' }))
+
+const inputClass = 'w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-800 shadow-sm outline-none transition focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 dark:border-dark-700 dark:bg-dark-900 dark:text-slate-100'
+const labelClass = 'mb-2 block text-xs font-semibold uppercase tracking-[0.24em] text-slate-500 dark:text-slate-400'
+
+const responseOnly = (responseText: string) => responseText.split('Response:').pop()?.trim() || responseText
+
+const InterviewWorkspacePage = () => {
+ const { token } = useAppSelector((state) => state.auth)
+ const [mode, setMode] = useState('')
+ const [participant, setParticipant] = useState({ display_name: '', department: '', job_title: '', notes: '' })
+ const [answers, setAnswers] = useState(emptyAnswers)
+ const [activeQuestion, setActiveQuestion] = useState(0)
+ const [records, setRecords] = useState([])
+ const [selectedRecord, setSelectedRecord] = useState(null)
+ const [analysis, setAnalysis] = useState(null)
+ const [isSaving, setIsSaving] = useState(false)
+ const [isLoading, setIsLoading] = useState(true)
+ const [notice, setNotice] = useState('')
+ const [error, setError] = useState('')
+
+ const answeredCount = useMemo(() => answers.filter((item) => item.answer.trim()).length, [answers])
+ const canSave = Boolean(mode) && answeredCount === QUESTIONS.length && !isSaving
+
+ const fetchRecords = async () => {
+ setIsLoading(true)
+ try {
+ const [recordsResponse, analysisResponse] = await Promise.all([
+ axios.get('/interview-workflow/records'),
+ axios.get('/interview-workflow/analysis'),
+ ])
+ const fetchedRecords = recordsResponse.data?.rows || []
+ setRecords(fetchedRecords)
+ setAnalysis(analysisResponse.data)
+ setSelectedRecord((current) => current || fetchedRecords[0] || null)
+ } catch (fetchError) {
+ if (axios.isAxiosError(fetchError) && fetchError.response?.status === 401) {
+ setError('Please log in to load the interview workspace.')
+ return
+ }
+ console.error('Failed to load interview workspace data', fetchError)
+ setError('Could not load saved interview records. Please refresh and try again.')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ if (token) {
+ fetchRecords()
+ }
+ }, [token])
+
+ const updateAnswer = (value: string) => {
+ setAnswers((current) => current.map((item, index) => (index === activeQuestion ? { answer: value } : item)))
+ }
+
+ const resetInterview = () => {
+ setMode('')
+ setParticipant({ display_name: '', department: '', job_title: '', notes: '' })
+ setAnswers(emptyAnswers)
+ setActiveQuestion(0)
+ setNotice('Ready for the next interview. Choose a response mode to begin.')
+ setError('')
+ }
+
+ const saveInterview = async () => {
+ if (!canSave || !mode) return
+
+ setIsSaving(true)
+ setError('')
+ setNotice('')
+
+ try {
+ const response = await axios.post('/interview-workflow/records', {
+ mode,
+ participant,
+ answers,
+ })
+ setNotice('Interview saved. A structured summary and findings were added to the repository.')
+ setSelectedRecord(response.data)
+ setRecords((current) => [response.data, ...current.filter((record) => record.id !== response.data.id)])
+ await fetchRecords()
+ } catch (saveError) {
+ console.error('Failed to save interview record', saveError)
+ setError(axios.isAxiosError(saveError) && saveError.response?.data ? String(saveError.response.data) : 'Could not save the interview. Please review the responses and try again.')
+ } finally {
+ setIsSaving(false)
+ }
+ }
+
+ const selectedResponses = [...(selectedRecord?.interview_responses_interview || [])].sort((a, b) => a.turn_number - b.turn_number)
+
+ return (
+ <>
+
+ {getPageTitle('Interview workspace')}
+
+
+
+
+
+
+
+
+
+
Facilitated assessment flow
+
Run the interview, capture every answer, and keep findings ready for trend analysis.
+
This workspace keeps the fixed five-question script on pace, saves a concise participant summary, and extracts reusable finding tags for future cross-interview analysis.
+
+
+
+
{records.length}
+
Saved
+
+
+
{answeredCount}/5
+
Current
+
+
+
≤60
+
Minutes
+
+
+
+
+
+ {notice &&
{notice}
}
+ {error &&
{error}
}
+
+
+
+
+
+
+
Step 1
+
Choose the session mode
+
Ask this first and keep the mode for the session unless the participant switches.
+
+
+
+
+
+
+
+
+
+
+
+
+ setParticipant({ ...participant, display_name: event.target.value })} placeholder="Optional, e.g. Participant A or HR manager" />
+