summaryrefslogtreecommitdiffhomepage
path: root/www/src
diff options
context:
space:
mode:
authorJay V <[email protected]>2025-05-26 16:36:30 -0400
committerJay V <[email protected]>2025-05-26 17:25:12 -0400
commitdeacf5991abfb777aae7823a8e7e352fbefdabd0 (patch)
treec38f80fa937298c17529c030d3e360ea92e45d72 /www/src
parent25623d1f84b6fa582f71b1b309f6e8235d4154a3 (diff)
downloadopencode-deacf5991abfb777aae7823a8e7e352fbefdabd0.tar.gz
opencode-deacf5991abfb777aae7823a8e7e352fbefdabd0.zip
Adding share page
Diffstat (limited to 'www/src')
-rw-r--r--www/src/components/Header.astro57
-rw-r--r--www/src/components/Share.tsx353
-rw-r--r--www/src/pages/share/[...id].astro35
3 files changed, 445 insertions, 0 deletions
diff --git a/www/src/components/Header.astro b/www/src/components/Header.astro
new file mode 100644
index 000000000..85cdc2ea5
--- /dev/null
+++ b/www/src/components/Header.astro
@@ -0,0 +1,57 @@
+---
+import config from 'virtual:starlight/user-config';
+import { Icon } from '@astrojs/starlight/components';
+import { HeaderLinks } from 'toolbeam-docs-theme/components';
+import Default from 'toolbeam-docs-theme/overrides/Header.astro';
+import SiteTitle from '@astrojs/starlight/components/SiteTitle.astro';
+
+const path = Astro.url.pathname;
+
+const links = config.social || [];
+---
+
+{ path.startsWith("/share/")
+ ? <div class="header sl-flex">
+ <div class="title-wrapper sl-flex">
+ <SiteTitle {...Astro.props} />
+ </div>
+ <div class="middle-group sl-flex">
+ <HeaderLinks {...Astro.props} />
+ </div>
+ </div>
+ : <Default {...Astro.props}><slot /></Default>
+}
+
+<style>
+ .header {
+ justify-content: space-between;
+ align-items: center;
+ height: 100%;
+ }
+
+ .title-wrapper {
+ /* Prevent long titles overflowing and covering the search and menu buttons on narrow viewports. */
+ overflow: clip;
+ /* Avoid clipping focus ring around link inside title wrapper. */
+ padding: calc(0.25rem + 2px) 0.25rem calc(0.25rem - 2px);
+ margin: -0.25rem;
+ }
+
+ .middle-group {
+ justify-content: flex-end;
+ gap: var(--sl-nav-gap);
+ }
+ @media (max-width: 50rem) {
+ :global(:root[data-has-sidebar]) {
+ .middle-group {
+ display: none;
+ }
+ }
+ }
+ @media (min-width: 50rem) {
+ .middle-group {
+ display: flex;
+ }
+ }
+</style>
+
diff --git a/www/src/components/Share.tsx b/www/src/components/Share.tsx
new file mode 100644
index 000000000..66748bcab
--- /dev/null
+++ b/www/src/components/Share.tsx
@@ -0,0 +1,353 @@
+import { createSignal, onCleanup, onMount, Show, For, createMemo } from "solid-js"
+
+type MessagePart = {
+ type: string
+ text?: string
+ [key: string]: any
+}
+
+type MessageContent = {
+ role?: string
+ parts?: MessagePart[]
+ metadata?: {
+ time?: {
+ created?: number
+ }
+ }
+ [key: string]: any
+}
+
+type Message = {
+ key: string
+ content: string
+}
+
+type SessionInfo = {
+ tokens?: {
+ input?: number
+ output?: number
+ reasoning?: number
+ }
+}
+
+export default function Share(props: { id: string, api: string }) {
+ const [messages, setMessages] = createSignal<Message[]>([])
+ const [connectionStatus, setConnectionStatus] = createSignal("Disconnected")
+ const [sessionInfo, setSessionInfo] = createSignal<SessionInfo | null>(null)
+
+ onMount(() => {
+ // Get the API URL from environment
+ const apiUrl = props.api
+ const shareId = props.id
+
+ console.log("Mounting Share component with ID:", shareId)
+ console.log("API URL:", apiUrl)
+
+ if (!shareId) {
+ console.error("Share ID not found in environment variables")
+ setConnectionStatus("Error: Share ID not found")
+ return
+ }
+
+ if (!apiUrl) {
+ console.error("API URL not found in environment variables")
+ setConnectionStatus("Error: API URL not found")
+ return
+ }
+
+ let reconnectTimer: number | undefined
+ let socket: WebSocket | null = null
+
+ // Function to create and set up WebSocket with auto-reconnect
+ const setupWebSocket = () => {
+ // Close any existing connection
+ if (socket) {
+ socket.close()
+ }
+
+ setConnectionStatus("Connecting...")
+
+ // Always use secure WebSocket protocol (wss)
+ const wsBaseUrl = apiUrl.replace(/^https?:\/\//, "wss://")
+ const wsUrl = `${wsBaseUrl}/share_poll?shareID=${shareId}`
+ console.log("Connecting to WebSocket URL:", wsUrl)
+
+ // Create WebSocket connection
+ socket = new WebSocket(wsUrl)
+
+ // Handle connection opening
+ socket.onopen = () => {
+ setConnectionStatus("Connected")
+ console.log("WebSocket connection established")
+ }
+
+ // Handle incoming messages
+ socket.onmessage = (event) => {
+ console.log("WebSocket message received")
+ try {
+ const data = JSON.parse(event.data) as Message
+
+ // Check if this is a session info message
+ if (data.key.startsWith("session/info/")) {
+ try {
+ const infoContent = JSON.parse(data.content) as SessionInfo;
+ setSessionInfo(infoContent);
+ console.log("Session info updated:", infoContent);
+ } catch (e) {
+ console.error("Error parsing session info:", e);
+ }
+ } else {
+ // For all other messages
+ setMessages((prev) => {
+ // Check if message with this key already exists
+ const existingIndex = prev.findIndex(msg => msg.key === data.key)
+
+ if (existingIndex >= 0) {
+ // Update existing message
+ const updated = [...prev]
+ updated[existingIndex] = data
+ return updated
+ } else {
+ // Add new message
+ return [...prev, data]
+ }
+ })
+ }
+ } catch (error) {
+ console.error("Error parsing WebSocket message:", error)
+ }
+ }
+
+ // Handle errors
+ socket.onerror = (error) => {
+ console.error("WebSocket error:", error)
+ setConnectionStatus("Error: Connection failed")
+ }
+
+ // Handle connection close and reconnection
+ socket.onclose = (event) => {
+ console.log(`WebSocket closed: ${event.code} ${event.reason}`)
+ setConnectionStatus("Disconnected, reconnecting...")
+
+ // Try to reconnect after 2 seconds
+ clearTimeout(reconnectTimer)
+ reconnectTimer = window.setTimeout(
+ setupWebSocket,
+ 2000,
+ ) as unknown as number
+ }
+ }
+
+ // Initial connection
+ setupWebSocket()
+
+ // Clean up on component unmount
+ onCleanup(() => {
+ console.log("Cleaning up WebSocket connection")
+ if (socket) {
+ socket.close()
+ }
+ clearTimeout(reconnectTimer)
+ })
+ })
+
+ return (
+ <main>
+ <h1>Share: {props.id}</h1>
+
+ <div style={{ margin: "2rem 0" }}>
+ <h2>WebSocket Connection</h2>
+ <p>
+ Status: <strong>{connectionStatus()}</strong>
+ </p>
+
+ <h3>Live Updates</h3>
+
+ <Show when={sessionInfo()}>
+ <div
+ style={{
+ backgroundColor: "#f8f9fa",
+ padding: "1rem",
+ borderRadius: "0.5rem",
+ marginBottom: "1rem",
+ border: "1px solid #dee2e6"
+ }}
+ >
+ <h4 style={{ margin: "0 0 0.75rem 0" }}>Session Information</h4>
+ <div style={{ display: "flex", gap: "1.5rem" }}>
+ <div>
+ <strong>Input Tokens:</strong> {sessionInfo()?.tokens?.input || 0}
+ </div>
+ <div>
+ <strong>Output Tokens:</strong> {sessionInfo()?.tokens?.output || 0}
+ </div>
+ <div>
+ <strong>Reasoning Tokens:</strong> {sessionInfo()?.tokens?.reasoning || 0}
+ </div>
+ </div>
+ </div>
+ </Show>
+
+ <div
+ style={{
+ border: "1px solid #ccc",
+ padding: "1rem",
+ borderRadius: "0.5rem",
+ maxHeight: "500px",
+ overflowY: "auto",
+ }}
+ >
+ <Show
+ when={messages().length > 0}
+ fallback={<p>Waiting for messages...</p>}
+ >
+ <ul style={{ listStyleType: "none", padding: 0 }}>
+ <For each={messages()}>
+ {(msg) => (
+ <li
+ style={{
+ padding: "0.75rem",
+ margin: "0.75rem 0",
+ backgroundColor: "#f5f5f5",
+ borderRadius: "0.5rem",
+ boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
+ }}
+ >
+ <div>
+ <strong>Key:</strong> {msg.key}
+ </div>
+
+ {(() => {
+ try {
+ const parsed = JSON.parse(msg.content) as MessageContent;
+ const createdTime = parsed.metadata?.time?.created
+ ? new Date(parsed.metadata.time.created).toLocaleString()
+ : 'Unknown time';
+
+ return (
+ <>
+ <div style={{ marginTop: "0.5rem" }}>
+ <strong>Full Content:</strong>
+ <pre
+ style={{
+ backgroundColor: "#f0f0f0",
+ padding: "0.5rem",
+ borderRadius: "0.25rem",
+ overflow: "auto",
+ maxHeight: "150px",
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ fontSize: "0.85rem",
+ }}
+ >
+ {JSON.stringify(parsed, null, 2)}
+ </pre>
+ </div>
+
+ {parsed.parts && parsed.parts.length > 0 && (
+ <div style={{ marginTop: "0.75rem" }}>
+ <div style={{
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+ padding: "0.25rem 0.5rem",
+ backgroundColor: "#e9ecef",
+ borderRadius: "0.25rem",
+ marginBottom: "0.5rem"
+ }}>
+ <strong>Role: {parsed.role || 'Unknown'}</strong>
+ <span style={{ fontSize: "0.8rem", color: "#6c757d" }}>
+ {createdTime}
+ </span>
+ </div>
+
+ <div style={{
+ backgroundColor: "#fff",
+ padding: "0.75rem",
+ borderRadius: "0.25rem",
+ border: "1px solid #dee2e6"
+ }}>
+ <For each={parsed.parts}>
+ {(part, index) => (
+ <div style={{ marginBottom: index() < parsed.parts!.length - 1 ? "0.75rem" : "0" }}>
+ {part.type === "text" ? (
+ <pre style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ fontFamily: "inherit",
+ margin: 0,
+ padding: 0,
+ backgroundColor: "transparent",
+ border: "none",
+ fontSize: "inherit",
+ overflow: "visible"
+ }}>
+ {part.text}
+ </pre>
+ ) : (
+ <div>
+ <div style={{
+ fontSize: "0.85rem",
+ fontWeight: "bold",
+ marginBottom: "0.25rem",
+ color: "#495057"
+ }}>
+ Part type: {part.type}
+ </div>
+ <pre
+ style={{
+ backgroundColor: "#f8f9fa",
+ padding: "0.5rem",
+ borderRadius: "0.25rem",
+ overflow: "auto",
+ maxHeight: "200px",
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ fontSize: "0.85rem",
+ margin: 0
+ }}
+ >
+ {JSON.stringify(part, null, 2)}
+ </pre>
+ </div>
+ )}
+ </div>
+ )}
+ </For>
+ </div>
+ </div>
+ )}
+ </>
+ );
+ } catch (e) {
+ return (
+ <div>
+ <strong>Content:</strong>
+ <pre
+ style={{
+ backgroundColor: "#f0f0f0",
+ padding: "0.5rem",
+ borderRadius: "0.25rem",
+ overflow: "auto",
+ maxHeight: "200px",
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {msg.content}
+ </pre>
+ </div>
+ );
+ }
+ })()}
+ </li>
+ )}
+ </For>
+ </ul>
+ </Show>
+ </div>
+ </div>
+ </main>
+ )
+}
+
diff --git a/www/src/pages/share/[...id].astro b/www/src/pages/share/[...id].astro
new file mode 100644
index 000000000..c3bc50d3b
--- /dev/null
+++ b/www/src/pages/share/[...id].astro
@@ -0,0 +1,35 @@
+---
+import config from "virtual:starlight/user-config";
+
+import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
+import Share from "../../components/Share.tsx";
+
+export const prerender = false;
+
+// TODO: Replace with API URL from environment
+
+const { id } = Astro.params;
+console.log(Astro.url.pathname);
+//console.log(config);
+---
+
+<StarlightPage
+ hasSidebar={false}
+ frontmatter={{
+ title: "Share",
+ pageFind: false,
+ template: "splash",
+ tableOfContents: false,
+ }}
+>
+ <Share id={id} api="https://api.dev.opencode.ai" client:only="solid" />
+</StarlightPage>
+
+<style is:global>
+body > .page > .main-frame .main-pane > main > .content-panel:first-of-type {
+ display: none;
+}
+body > .page > .main-frame .main-pane > main > .content-panel + .content-panel {
+ border-top: none;
+}
+</style>