Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { NextRequest } from "next/server"
import { transcriptionSessionStore } from "@transcript-assembly"
import { requireAuth } from "@/lib/auth"

export const runtime = "nodejs"

Expand All @@ -11,6 +12,14 @@ export async function GET(
req: NextRequest,
context: { params: Promise<{ sessionId: string }> | { sessionId: string } },
) {
const auth = await requireAuth(req)
if (!auth) {
return new Response(JSON.stringify({ error: { code: "unauthorized", message: "Authentication required" } }), {
status: 401,
headers: { "Content-Type": "application/json" },
})
}

const resolvedParams = "then" in context.params ? await context.params : context.params
const { sessionId } = resolvedParams
let cleanup: (() => void) | null = null
Expand Down
83 changes: 73 additions & 10 deletions apps/web/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { useEncounters, EncounterList, IdleView, NewEncounterForm, RecordingView
import { NoteEditor } from "@note-rendering"
import { useAudioRecorder, type RecordedSegment, warmupMicrophonePermission, warmupSystemAudioPermission } from "@audio"
import { useSegmentUpload, type UploadError } from "@transcription";
import { generateClinicalNote } from "@/app/actions"
import {
getPreferences,
setPreferences,
Expand Down Expand Up @@ -82,6 +81,18 @@ function resolveApiBaseUrl(): string {
return "http://localhost:3001"
}

function isHostedModeClient(): boolean {
return String(process.env.NEXT_PUBLIC_HOSTED_MODE || "").toLowerCase() === "true"
}

function getStoredIdToken(): string | null {
if (typeof window === "undefined") return null
const fromStorage = window.localStorage.getItem("openscribe_id_token")
if (fromStorage) return fromStorage
const fromGlobal = window.__OPENSCRIBE_ID_TOKEN
return typeof fromGlobal === "string" && fromGlobal.length > 0 ? fromGlobal : null
}

function HomePageContent() {
const { encounters, addEncounter, updateEncounter, deleteEncounter: removeEncounter, refresh } = useEncounters()

Expand All @@ -91,7 +102,7 @@ function HomePageContent() {
const [view, setView] = useState<ViewState>({ type: "idle" })
const [transcriptionStatus, setTranscriptionStatus] = useState<StepStatus>("pending")
const [noteGenerationStatus, setNoteGenerationStatus] = useState<StepStatus>("pending")
const [processingMetrics, setProcessingMetrics] = useState<ProcessingMetrics>({})
const [, setProcessingMetrics] = useState<ProcessingMetrics>({})
const [sessionId, setSessionId] = useState<string | null>(null)

const currentEncounterIdRef = useRef<string | null>(null)
Expand All @@ -112,7 +123,7 @@ function HomePageContent() {
const [localDurationMs, setLocalDurationMs] = useState(0)
const [localPaused, setLocalPaused] = useState(false)
const localSessionNameRef = useRef<string | null>(null)
const localBackendRef = useRef<Window["desktop"]["openscribeBackend"] | null>(null)
const localBackendRef = useRef<NonNullable<Window["desktop"]>["openscribeBackend"] | null>(null)
const localLastTickRef = useRef<number | null>(null)

useEffect(() => {
Expand Down Expand Up @@ -217,8 +228,33 @@ function HomePageContent() {
const { enqueueSegment, resetQueue } = useSegmentUpload(sessionId, {
onError: handleUploadError,
apiBaseUrl: apiBaseUrlRef.current || undefined,
getAuthToken: async () => {
if (!isHostedModeClient()) return null
return getStoredIdToken()
},
})

useEffect(() => {
if (!isHostedModeClient()) return
const token = getStoredIdToken()
if (!token) return

const baseUrl = apiBaseUrlRef.current
const url = baseUrl
? `${baseUrl.replace(/\/+$/, "")}/api/auth/bootstrap`
: "/api/auth/bootstrap"

void fetch(url, {
method: "POST",
credentials: "include",
headers: {
Authorization: `Bearer ${token}`,
},
}).catch((error) => {
debugWarn("Hosted bootstrap call failed", error)
})
}, [])

const cleanupSession = useCallback(() => {
debugLog('[Cleanup] Closing EventSource connection')
if (eventSourceRef.current) {
Expand Down Expand Up @@ -379,13 +415,33 @@ function HomePageContent() {
noteGenerationStartedAt: prev.noteGenerationStartedAt ?? Date.now(),
}))
try {
const note = await generateClinicalNote({
transcript,
patient_name: patientName,
visit_reason: visitReason,
noteLength: noteLengthRef.current,
template,
const baseUrl = apiBaseUrlRef.current
const url = baseUrl ? `${baseUrl.replace(/\/+$/, "")}/api/notes/generate` : "/api/notes/generate"
const token = isHostedModeClient() ? getStoredIdToken() : null
const headers: Record<string, string> = {
"Content-Type": "application/json",
}
if (token) {
headers.Authorization = `Bearer ${token}`
}
const response = await fetch(url, {
method: "POST",
headers,
credentials: "include",
body: JSON.stringify({
transcript,
patient_name: patientName,
visit_reason: visitReason,
noteLength: noteLengthRef.current,
template,
}),
})
if (!response.ok) {
const errBody = (await response.json().catch(() => ({}))) as { error?: { message?: string } }
throw new Error(errBody?.error?.message || `Note generation failed (${response.status})`)
}
const body = (await response.json()) as { note: string }
const note = body.note
await updateEncounterRef.current(encounterId, {
note_text: note,
status: "completed",
Expand Down Expand Up @@ -595,8 +651,15 @@ function HomePageContent() {
const url = baseUrl
? `${baseUrl.replace(/\/+$/, "")}/api/transcription/final`
: "/api/transcription/final"
const token = isHostedModeClient() ? getStoredIdToken() : null
const headers: Record<string, string> = {}
if (token) {
headers.Authorization = `Bearer ${token}`
}
const response = await fetch(url, {
method: "POST",
headers,
credentials: "include",
body: formData,
})
if (!response.ok) {
Expand Down Expand Up @@ -737,7 +800,7 @@ function HomePageContent() {
processingEndedAt: Date.now(),
}))
}
} catch (error) {
} catch {
setTranscriptionStatus("failed")
setNoteGenerationStatus("failed")
setProcessingMetrics((prev) => ({
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/types/desktop.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ declare global {
readEntries: (filter?: unknown) => Promise<unknown[]>
exportLog: (options: { data: string; filename: string }) => Promise<{ success: boolean; canceled?: boolean; filePath?: string; error?: string }>
}
openscribeBackend?: {
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>
on: (channel: string, listener: (event: unknown, payload: unknown) => void) => void
removeAllListeners: (channel: string) => void
}
}

interface Window {
desktop?: DesktopAPI
__openscribePermissionsPrimed?: boolean
__OPENSCRIBE_ID_TOKEN?: string
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface UploadError {
export interface SegmentUploadControllerOptions {
onError?: (error: UploadError) => void
apiBaseUrl?: string
getAuthToken?: () => Promise<string | null>
}

export interface SegmentUploadControllerDeps {
Expand Down Expand Up @@ -48,6 +49,7 @@ export class SegmentUploadController {
private readonly waitFn: (ms: number) => Promise<void>
private readonly options?: SegmentUploadControllerOptions
private readonly apiBaseUrl?: string
private readonly getAuthToken?: () => Promise<string | null>

constructor(
sessionId: string | null,
Expand All @@ -57,6 +59,7 @@ export class SegmentUploadController {
this.sessionId = sessionId
this.options = options
this.apiBaseUrl = options?.apiBaseUrl
this.getAuthToken = options?.getAuthToken
this.fetchFn = deps?.fetchFn ?? globalThis.fetch.bind(globalThis)
if (!this.fetchFn) {
throw new Error("fetch API is required for SegmentUploadController")
Expand Down Expand Up @@ -145,8 +148,16 @@ export class SegmentUploadController {
const url = this.apiBaseUrl
? `${this.apiBaseUrl.replace(/\/+$/, "")}/api/transcription/segment`
: "/api/transcription/segment"
const token = this.getAuthToken ? await this.getAuthToken() : null
const headers: Record<string, string> = {}
if (token) {
headers.Authorization = `Bearer ${token}`
}

const response = await this.fetchFn(url, {
method: "POST",
headers,
credentials: "include",
body: formData,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function useSegmentUpload(sessionId: string | null, options?: UseSegmentU
controllerRef.current = new SegmentUploadController(sessionId, {
onError: (error) => onErrorRef.current?.(error),
apiBaseUrl: options?.apiBaseUrl,
getAuthToken: options?.getAuthToken,
})
} else {
// Update sessionId synchronously to avoid race condition with first segment
Expand Down