-
❓ .gitignoreUnknown, 37 B⬇
-
❓ README.mdUnknown, 2.94 kB⬇
-
🌐 admin_panel.phpWeb, 1.54 kB⬇
-
🌐 api.jsWeb, 22.21 kB⬇
-
🌐 api.phpWeb, 9.62 kB⬇
-
🌐 app.jsWeb, 61.42 kB⬇
-
🌐 attachments.jsWeb, 7.30 kB⬇
-
🌐 base.cssWeb, 2.23 kB⬇
-
🌐 choices-editor.jsWeb, 28.11 kB⬇
-
🌐 choices.cssWeb, 1.14 kB⬇
-
🌐 content.jsWeb, 31.08 kB⬇
-
🌐 context.jsWeb, 9.26 kB⬇
-
🌐 conversation-index.jsWeb, 10.74 kB⬇
-
🌐 conversation.cssWeb, 16.33 kB⬇
-
🌐 conversation.jsWeb, 1.11 kB⬇
-
🌐 dialogs.cssWeb, 11.43 kB⬇
-
🌐 diction.jsWeb, 948 B⬇
-
🌐 document-index.jsWeb, 1.04 kB⬇
-
🌐 document-model.jsWeb, 12.88 kB⬇
-
🌐 document.cssWeb, 6.74 kB⬇
-
🌐 document.jsWeb, 44.96 kB⬇
-
🌐 dynamic.phpWeb, 533 B⬇
-
📷 favicon-lg.pngImage, 9.11 kB⬇
-
📷 favicon.pngImage, 3.12 kB⬇
-
🌐 index-lists.cssWeb, 4.77 kB⬇
-
🌐 index.phpWeb, 49.72 kB⬇
-
🌐 indexjs.jsWeb, 377 B⬇
-
🌐 instant-answer.cssWeb, 5.17 kB⬇
-
🌐 layout.cssWeb, 11.37 kB⬇
-
🌐 login.phpWeb, 997 B⬇
-
❓ manifest.webmanifestUnknown, 262 B⬇
-
🌐 memory.jsWeb, 407 B⬇
-
🌐 mermaid.cssWeb, 6.16 kB⬇
-
🌐 model-presets.jsonWeb, 5.51 kB⬇
-
🌐 models.jsonWeb, 8.05 kB⬇
-
🌐 notes.cssWeb, 11.97 kB⬇
-
🌐 overlays.cssWeb, 13.97 kB⬇
-
⚠️ php.iniOS File, 424 B⬇
-
🌐 rankings-view.jsWeb, 5.67 kB⬇
-
🌐 rankings.cssWeb, 3.35 kB⬇
-
🌐 scratchpad-conversation.jsWeb, 4.87 kB⬇
-
🌐 scratchpad-index.jsWeb, 15.55 kB⬇
-
🌐 scratchpad-model.jsWeb, 13.29 kB⬇
-
🌐 scratchpad.cssWeb, 3.12 kB⬇
-
🌐 settings.cssWeb, 4.65 kB⬇
-
🌐 shared-view.cssWeb, 3.48 kB⬇
-
🌐 shared.cssWeb, 16.26 kB⬇
-
🌐 shared.jsWeb, 14.63 kB⬇
-
🌐 shared.phpWeb, 3.28 kB⬇
-
🌐 sticky-notes-editor.jsWeb, 64.84 kB⬇
-
🌐 storage.jsWeb, 36.73 kB⬇
-
🌐 style.cssWeb, 12.06 kB⬇
-
🌐 sw.jsWeb, 1.69 kB⬇
-
🌐 sync.jsWeb, 19.02 kB⬇
-
🌐 tools-index.jsWeb, 15.00 kB⬇
-
🌐 users.jsWeb, 49.86 kB⬇
-
🌐 utility-models.jsonWeb, 1.04 kB⬇
-
🌐 voice-models.jsonWeb, 4.81 kB⬇
/**
* Read-only viewer for shared conversations (shared.php). Fetches the copied
* conversation from the public share endpoint, renders it by conversation
* type with the app's renderers in read-only mode, and provides the page's
* own zoom, color theme and show-costs settings plus export. No session and
* no cost-incurring calls: the code overlay's Analyze action is absent and
* read-aloud controls are not rendered.
*/
import './util/scrollbars.js';
import Conversation from './conversation.js';
import ScratchpadConversation from './scratchpad-conversation.js';
import Document from './document.js';
import RankingsView from './rankings-view.js';
import ChoicesEditor from './choices-editor.js';
import StickyNotesEditor from './sticky-notes-editor.js';
import { compute_list_markers } from './document-model.js';
import {
getEl, update_range_fill
} from './util/dom-utils.js';
import {
highlightCodeBlocks, typesetMathjax, extractFormattedContent,
build_export_html, rasterize_diagrams
} from './util/format-utils.js';
import { copyToClipboard } from './util/async-utils.js';
import {
applyTheme, fetchThemeIndex, populateThemeFamilyDropdown,
populateThemeDropdown, getThemeFamilyForTheme, fadeOutLoader
} from './util/theme-utils.js';
import { render_mermaid_in_container } from './util/mermaid-render.js';
import { init_code_overlay, attach_code_expand_buttons } from './util/code-overlay.js';
import { init_table_overlay, attach_table_expand_buttons } from './util/table-overlay.js';
import { init_diagram_overlay } from './util/diagram-overlay.js';
import { export_content } from './util/export-utils.js';
import { exportDocumentPdf } from './util/pdf-export.js';
import { customAlert } from './util/confirm-dialog.js';
const THEME_KEY = 'SHARED_VIEWER_THEME';
const ZOOM_KEY = 'SHARED_VIEWER_ZOOM';
const COSTS_KEY = 'SHARED_VIEWER_COSTS';
const THINKING_KEY = 'SHARED_VIEWER_THINKING';
let conversation = null;
/**
* Fetches the shared copy for the ?g= guid from the public endpoint.
* @returns {Promise<Object>} The conversation object.
*/
async function fetch_shared_conversation() {
const guid = new URLSearchParams(location.search).get('g') || '';
const response = await fetch('api.php?function=share&action=get', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'get', g: guid })
});
const data = await response.json();
if (!response.ok || data.error) throw new Error(data.error || 'Failed to load the shared conversation.');
return data.conversation;
}
/**
* Shows a status message in place of the content (load errors).
* @param {string} message - The message text.
*/
function show_status(message) {
const status = getEl('id-div-shared-status');
status.textContent = message;
status.style.display = 'block';
getEl('id-div-shared-chat').style.display = 'none';
}
/**
* Returns the stored settings from localStorage with defaults applied.
* @returns {Object} { theme, zoom, costs, thinking }.
*/
function load_settings() {
const zoom = parseInt(localStorage.getItem(ZOOM_KEY), 10);
return {
theme: localStorage.getItem(THEME_KEY) || 'theme_dark',
zoom: Number.isFinite(zoom) && zoom >= 70 && zoom <= 200 ? zoom : 100,
costs: localStorage.getItem(COSTS_KEY) === '1',
thinking: localStorage.getItem(THINKING_KEY) === '1'
};
}
/**
* Applies the stored zoom percentage to the root element, like the app's UI zoom.
* @param {number} zoom - Zoom percentage (70-200).
*/
function apply_zoom(zoom) {
document.documentElement.style.zoom = zoom === 100 ? '' : String(zoom / 100);
}
/**
* Applies or removes the body flag that hides the per-turn cost tables.
* @param {boolean} show - Whether costs are shown.
*/
function apply_costs(show) {
document.body.classList.toggle('show-costs', show);
}
/**
* Applies or removes the body flag that hides the thought-process blocks.
* @param {boolean} show - Whether thinking is shown.
*/
function apply_thinking(show) {
document.body.classList.toggle('show-thinking', show);
}
/**
* Wires the settings bar: theme selects, zoom slider and show-costs toggle.
*/
async function init_settings() {
const settings = load_settings();
const family_select = getEl('id-select-shared-theme-family');
const theme_select = getEl('id-select-shared-theme');
const theme_index = await fetchThemeIndex();
const family = getThemeFamilyForTheme(settings.theme, theme_index);
populateThemeFamilyDropdown(family_select, theme_index, family);
populateThemeDropdown(theme_select, theme_index, family, settings.theme);
family_select.addEventListener('change', () => {
populateThemeDropdown(theme_select, theme_index, family_select.value, settings.theme);
});
theme_select.addEventListener('change', () => {
localStorage.setItem(THEME_KEY, theme_select.value);
settings.theme = theme_select.value;
void applyTheme(settings.theme);
});
await applyTheme(settings.theme);
const zoom_input = getEl('id-input-shared-zoom');
const zoom_value = getEl('id-span-shared-zoom-value');
zoom_input.value = String(settings.zoom);
zoom_value.textContent = settings.zoom + '%';
update_range_fill(zoom_input);
apply_zoom(settings.zoom);
zoom_input.addEventListener('input', () => {
const zoom = parseInt(zoom_input.value, 10);
localStorage.setItem(ZOOM_KEY, String(zoom));
zoom_value.textContent = zoom + '%';
apply_zoom(zoom);
update_range_fill(zoom_input);
});
const costs_checkbox = getEl('id-checkbox-shared-costs');
costs_checkbox.checked = settings.costs;
apply_costs(settings.costs);
costs_checkbox.addEventListener('change', () => {
localStorage.setItem(COSTS_KEY, costs_checkbox.checked ? '1' : '0');
apply_costs(costs_checkbox.checked);
});
const thinking_checkbox = getEl('id-checkbox-shared-thinking');
thinking_checkbox.checked = settings.thinking;
apply_thinking(settings.thinking);
thinking_checkbox.addEventListener('change', () => {
localStorage.setItem(THINKING_KEY, thinking_checkbox.checked ? '1' : '0');
apply_thinking(thinking_checkbox.checked);
});
}
/**
* Renders the conversation by its type into the right container and returns
* the element holding the rendered content.
* @param {Object} conversation - The shared conversation copy.
* @returns {HTMLElement} The content root.
*/
function render_conversation(conversation) {
const type = conversation.TYPE || 'CHAT';
const chat_scroll = getEl('id-div-shared-chat');
const document_container = getEl('div-shared-document-container');
const content = getEl('id-div-shared-content');
const settings = {
read_only: true,
show_suggested: conversation.SHOW_SUGGESTED_QUERIES === true,
show_related: conversation.SHOW_RELATED_QUERIES === true
};
if (type === 'DOCUMENT') {
chat_scroll.style.display = 'none';
document_container.style.display = 'block';
const items = Array.isArray(conversation.DOCUMENT) ? conversation.DOCUMENT : [];
document_container.querySelector('.div-document-items').innerHTML = new Document().render(items);
return document_container.querySelector('.div-document-items');
}
let html = '';
if (type === 'SCRATCHPAD') {
html = new ScratchpadConversation().render(conversation, settings);
} else if (type === 'RANKING') {
html = new RankingsView().render(Array.isArray(conversation.RANKINGS) ? conversation.RANKINGS : [], settings);
} else if (type === 'COLORS') {
html = new ChoicesEditor().render(Array.isArray(conversation.CHOICES) ? conversation.CHOICES : []);
} else if (type === 'STICKY_NOTES') {
html = new StickyNotesEditor().render(Array.isArray(conversation.STICKIES) ? conversation.STICKIES : [], Array.isArray(conversation.TAGS) ? conversation.TAGS : []);
} else {
html = new Conversation().render(conversation, settings);
}
content.innerHTML = html;
return content;
}
/**
* Makes a rendered root read-only: strips contenteditable and disables every
* control except the per-turn copy buttons.
* @param {HTMLElement} root - The rendered content root.
*/
function make_read_only(root) {
root.querySelectorAll('[contenteditable]').forEach(el => el.removeAttribute('contenteditable'));
root.querySelectorAll('button, input, select, textarea').forEach(el => {
if (!el.classList.contains('btn-copy-response') && !el.classList.contains('btn-copy-scratchpad')) {
el.disabled = true;
}
});
}
/**
* Moves each per-turn copy button into the response content it copies, so
* it can hover in that content's top-right corner (shared-view.css), and
* removes the now-empty toolbar rows.
* @param {HTMLElement} root - The rendered content root.
*/
function relocate_copy_buttons(root) {
root.querySelectorAll('.btn-copy-response, .btn-copy-scratchpad').forEach(btn => {
const response_div = root.querySelector(`.div-response-content[data-index="${btn.getAttribute('data-index')}"]`);
if (response_div) {
response_div.appendChild(btn);
}
});
root.querySelectorAll('.div-chat-response-buttons').forEach(el => el.remove());
}
/**
* Binds the interactive behaviour the viewer keeps: per-turn copy, thinking
* block toggles, and code copy buttons rendered inside code blocks.
* @param {HTMLElement} root - The rendered content root.
*/
function bind_viewer_controls(root) {
root.querySelectorAll('.btn-copy-response, .btn-copy-scratchpad').forEach(btn => {
btn.onclick = () => {
const response_div = root.querySelector(`.div-response-content[data-index="${btn.getAttribute('data-index')}"]`);
if (response_div) {
void copyToClipboard(response_div, btn);
}
};
});
root.querySelectorAll('.p-thinking-header').forEach(header => {
header.onclick = () => {
const container = header.closest('.div-thinking-content');
if (!container) return;
const is_expanded = container.classList.contains('thinking-expanded');
const arrow = header.querySelector('.span-thinking-arrow');
const text_div = container.querySelector('.div-thinking-text');
container.classList.toggle('thinking-expanded', !is_expanded);
container.classList.toggle('thinking-collapsed', is_expanded);
if (arrow) arrow.textContent = is_expanded ? '\u25b6' : '\u25bc';
if (text_div) text_div.style.display = is_expanded ? 'none' : 'block';
};
});
root.querySelectorAll('.copyable-code, .copyable-table').forEach(btn => {
btn.onclick = () => {
const data = btn.getAttribute('data-code') || btn.getAttribute('data-table');
void copyToClipboard(data || '', btn);
};
});
}
/**
* Waits briefly for the async MathJax script so equations typeset on first
* render even when the library is still loading.
* @returns {Promise<void>}
*/
async function wait_mathjax(timeout_ms = 5000) {
const start = Date.now();
while (!globalThis.MathJax?.typesetPromise) {
if (Date.now() - start > timeout_ms) return;
await new Promise(resolve => setTimeout(resolve, 100));
}
}
/**
* Runs the post-render pass over a rendered root: highlighting, diagrams,
* overlay buttons, equations, read-only enforcement and viewer controls.
* @param {HTMLElement} root - The rendered content root.
*/
async function post_render(root) {
make_read_only(root);
relocate_copy_buttons(root);
highlightCodeBlocks(root);
await render_mermaid_in_container(root);
attach_code_expand_buttons(root);
attach_table_expand_buttons(root);
await wait_mathjax();
await typesetMathjax(root);
bind_viewer_controls(root);
}
/**
* Returns the conversation title for the header and file names.
* @param {Object} conversation - The shared conversation copy.
* @returns {string} The title.
*/
function conversation_title(conversation) {
return String(conversation?.TITLE || '').trim() || 'Shared Conversation';
}
/**
* Builds a detached clone of the rendered content prepared for export:
* copy buttons, expand buttons and the sticky controls removed.
* @param {HTMLElement} root - The rendered content root.
* @returns {HTMLElement} The prepared clone.
*/
function prepared_export_clone(root) {
const clone = root.cloneNode(true);
clone.querySelectorAll('.btn-copy-response, .btn-copy-scratchpad, .div-chat-response-buttons, .div-code-expand-btn, .div-diagram-expand-btn, .div-notes-sticky, .div-notes-note-actions').forEach(el => el.remove());
return clone;
}
/**
* Resolves the typed document/scratchpad items for the PDF export, or null
* for types exported from the rendered DOM only.
* @returns {Array|null} The content items.
*/
function pdf_items() {
const type = conversation?.TYPE;
if (type === 'DOCUMENT') {
return Array.isArray(conversation.DOCUMENT) ? conversation.DOCUMENT : [];
}
if (type === 'SCRATCHPAD') {
const history = Array.isArray(conversation.HISTORY) ? conversation.HISTORY : [];
const revision = history[history.length - 1];
return Array.isArray(revision?.items) ? revision.items : [];
}
return null;
}
/**
* Wires the export dropdown: HTML variants and markdown are built from the
* rendered DOM; PDF is offered for document and scratchpad types, whose
* typed items the PDF exporter consumes. Sticky notes and rankings have no
* export: the dropdown is hidden for them.
* @param {HTMLElement} root - The rendered content root.
*/
function init_export(root) {
const select = getEl('id-select-shared-export');
const type = conversation?.TYPE;
if (type === 'STICKY_NOTES' || type === 'RANKING') {
select.style.display = 'none';
return;
}
const items = pdf_items();
if (!items) {
select.querySelector('.option-shared-pdf')?.remove();
}
select.addEventListener('change', async () => {
const format = select.value;
select.value = '';
if (!format) return;
const title = conversation_title(conversation);
try {
const images = await rasterize_diagrams(root, items || []);
await export_content(format, title, {
pdf: () => exportDocumentPdf(items, compute_list_markers(items), title, images),
html: async (mobile) => {
const { html } = await extractFormattedContent(prepared_export_clone(root), images);
return build_export_html(title, html, mobile);
},
markdown: async () => {
const { text } = await extractFormattedContent(prepared_export_clone(root), images);
return text + '\n';
}
});
} catch (error) {
void customAlert('Export Failed', String(error?.message || error));
}
});
}
/**
* Boots the viewer: fetch, render, settings, overlays and export.
*/
async function init() {
init_diagram_overlay();
init_table_overlay();
init_code_overlay({
load_analysis: () => null,
save_analysis: () => {},
single_column_breakpoint: 780
});
try {
conversation = await fetch_shared_conversation();
} catch (error) {
show_status(String(error?.message || error));
fadeOutLoader();
return;
}
document.title = conversation_title(conversation) + ' - TinAI';
getEl('id-span-shared-title').textContent = conversation_title(conversation);
await init_settings();
const root = render_conversation(conversation);
await post_render(root);
init_export(root);
fadeOutLoader();
}
init();
shared.js
×
Type: Web, text/x-java
14.63 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File
Type: Web, text/x-java
14.63 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File