/**
* Generates `lib/ai-chat/session-store.js`, the home of the chat transcript
* while the visitor moves around the site.
*
* WHY THIS EXISTS. The chat is a component instance placed on each page, so
* navigating unmounts it and React throws its state away — the visitor loses
* the conversation by clicking a link.
*
* TWO LAYERS, deliberately:
*
* - a `globalThis` bag, which carries the transcript through a client-side
* navigation (Next's ``) without touching storage at all;
* - `sessionStorage`, which carries it through a FULL document load — the
* language switcher is a plain ``, and typing a URL does the
* same thing. Both tear down the JavaScript context, so an in-memory bag
* alone loses the conversation exactly when the visitor changes language.
*
* ⛔ sessionStorage would also survive a refresh, which is NOT wanted: a
* refresh starts a new conversation. That is what the reload check is for —
* `PerformanceNavigationTiming.type` tells a reload apart from a navigation,
* so the stored transcript is dropped on F5 and kept on everything else.
* It is scoped to one tab by definition, so a second tab starts clean.
*
* The conversation id travels with the transcript because the two are one
* thing: the server appends to a conversation row by id (`ON CONFLICT (id) DO
* UPDATE`), so restoring messages without the id would show the visitor a
* history the database no longer believes in, and restoring the id without the
* messages would leave the assistant answering with no visible context.
*/
/** Emitted next to `localized-messages`, under `lib/ai-chat/`. */
export const AI_CHAT_SESSION_STORE_FILE = 'session-store'
export function generateAIChatSessionStoreCode(): string {
return `// AI assistant chat session — survives navigation, dies on refresh.
// Generated by TeleportHQ. Do not edit.
var MEMORY_KEY = '__tqAiChatSession'
var STORAGE_KEY = 'tq-ai-chat-session'
/**
* Whether THIS document was reloaded (F5, Cmd-R, location.reload) as opposed
* to navigated to. A back/forward move reports 'back_forward' and counts as
* navigation, so the conversation survives it.
*/
function isPageReload() {
try {
var entries = performance.getEntriesByType('navigation')
if (entries && entries.length > 0) {
return entries[0].type === 'reload'
}
// Older browsers: 1 === TYPE_RELOAD.
if (performance.navigation) {
return performance.navigation.type === 1
}
} catch (error) {
/* performance API unavailable — treat as a normal navigation */
}
return false
}
// Every storage call is wrapped: Safari private mode throws on write, and a
// browser set to block site data throws on read. Losing the cross-document
// hand-off is survivable; throwing inside a render is not.
function readStoredSession() {
try {
return window.sessionStorage.getItem(STORAGE_KEY)
} catch (error) {
return null
}
}
function writeStoredSession(value) {
try {
window.sessionStorage.setItem(STORAGE_KEY, value)
} catch (error) {
/* private mode or quota — the in-memory copy still covers client-side navigation */
}
}
function removeStoredSession() {
try {
window.sessionStorage.removeItem(STORAGE_KEY)
} catch (error) {
/* nothing to do */
}
}
/**
* Drops the stored conversation when this document was reloaded.
*
* Runs at MODULE LOAD, not on first use. A refresh on a page without the chat
* would otherwise leave the transcript in storage untouched, and the next chat
* page — a plain navigation — would happily restore it, so the conversation
* would outlive the refresh that was meant to end it. The app shell imports
* this module for exactly that reason, so the check runs on every page.
*/
function purgeSessionOnReload() {
if (typeof window === 'undefined') {
return
}
if (isPageReload()) {
removeStoredSession()
}
}
purgeSessionOnReload()
/**
* The session for this document, hydrated from storage the first time it is
* asked for. Returns null on the server.
*
* The SSR guard is not optional: one Node process serves every visitor, so a
* global touched during server rendering would leak one person's conversation
* into another's HTML and break hydration on top of it.
*/
function getSession() {
if (typeof window === 'undefined') {
return null
}
var scope = typeof globalThis !== 'undefined' ? globalThis : window
if (scope[MEMORY_KEY]) {
return scope[MEMORY_KEY]
}
var session = { messages: [], conversationId: '' }
scope[MEMORY_KEY] = session
var stored = readStoredSession()
if (stored) {
try {
var parsed = JSON.parse(stored)
if (parsed && Array.isArray(parsed.messages)) {
session.messages = parsed.messages
session.conversationId =
typeof parsed.conversationId === 'string' ? parsed.conversationId : ''
}
} catch (error) {
// Written by an older version, or truncated — start clean rather than
// hand React a shape it cannot render.
removeStoredSession()
}
}
return session
}
/**
* The transcript from an earlier page, or \`fallback\` (the welcome bubble) when
* this is the first page of the visit.
*/
export function restoreAIChatMessages(fallback) {
var session = getSession()
if (!session || !Array.isArray(session.messages) || session.messages.length === 0) {
return fallback
}
return session.messages
}
/** The conversation the visitor is already in, so replies keep appending to it. */
export function restoreAIChatConversationId(fallback) {
var session = getSession()
if (!session || typeof session.conversationId !== 'string' || !session.conversationId) {
return fallback
}
return session.conversationId
}
/** Mirrors the live state, so the next page picks up exactly where this left off. */
export function persistAIChatSession(messages, conversationId) {
var session = getSession()
if (!session) {
return
}
session.messages = Array.isArray(messages) ? messages : []
session.conversationId = typeof conversationId === 'string' ? conversationId : ''
// Only the welcome bubble means nothing worth carrying across a document
// load; keeping storage empty also keeps a fresh tab genuinely fresh.
if (session.messages.length <= 1 && !session.conversationId) {
removeStoredSession()
return
}
try {
writeStoredSession(
JSON.stringify({ messages: session.messages, conversationId: session.conversationId })
)
} catch (error) {
/* a message that cannot be serialised must not break the chat */
}
}
/** Drops the conversation, so the next mount starts from the welcome message. */
export function clearAIChatSession() {
var session = getSession()
if (session) {
session.messages = []
session.conversationId = ''
}
removeStoredSession()
}
`
}