Skip to content
Open
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
32 changes: 22 additions & 10 deletions websites/G/Grok/Grok.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
{
"grok.talkingWithAI": {
"description": "Displayed when the user is engaging in a conversation with the AI.",
"message": "Talking with AI about something"
"message": "Talking with Grok"
},
"grok.aiResponding": {
"description": "Displayed when the AI is generating a response.",
"message": "AI is responding..."
},
"grok.conversationStats": {
"description": "Displayed to show the number of times the user asked a question and total words used, e.g., 'asked 3 times | 150 words'.",
"message": "asked {0} times | {1} words"
"grok.replyingTo": {
"description": "Displayed as the state line under the chat title. {0} is the user's latest prompt.",
"message": "Replying to: \"{0}\""
},
"grok.startNewConversation": {
"description": "Button text to start a new conversation.",
"description": "Displayed when the user is on the new chat screen.",
"message": "Start new conversation"
},
"grok.thinkingOfPrompt": {
"description": "Displayed when the AI is thinking of a new prompt.",
"description": "Displayed when the user is thinking of a new prompt.",
"message": "Thinking of a new prompt..."
},
"grok.creatingImages": {
"description": "Displayed when the user is on Grok Imagine.",
"message": "Creating with Imagine"
},
"grok.viewingFiles": {
"description": "Displayed when the user is browsing files or the library.",
"message": "Browsing files"
},
"grok.browsingProjects": {
"description": "Displayed when the user is in projects or workspaces.",
"message": "Browsing projects"
},
"grok.readingSharedChat": {
"description": "Displayed when the user is viewing a shared conversation.",
"message": "Reading a shared chat"
}
}
9 changes: 9 additions & 0 deletions websites/G/Grok/assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export enum ActivityAssets {
Logo = 'https://i.imgur.com/KygGP1w.png',
Fast = 'https://i.imgur.com/uJSu7il.png',
Auto = 'https://i.imgur.com/S5ACb1x.png',
Expert = 'https://i.imgur.com/FiP0gX4.png',
Heavy = 'https://i.imgur.com/5miQ3Fc.png',
Build = 'https://i.imgur.com/IxrTU1f.png',
Voice = 'https://i.imgur.com/OvDt2f1.png',
}
212 changes: 212 additions & 0 deletions websites/G/Grok/conversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
export interface ConversationInfo {
id: string | null
title: string | null
lastPrompt: string | null
isGenerating: boolean
}

interface ConversationCache {
id: string
title: string | null
lastPrompt: string | null
fetchedAt: number
}

const FETCH_INTERVAL = 12_000
let cache: ConversationCache | null = null
let inflight: Promise<void> | null = null

function collapse(text: string): string {
return text.replace(/\s+/g, ' ').trim()
}

function truncate(text: string, max: number): string {
const value = collapse(text)
if (value.length <= max)
return value
return `${value.slice(0, Math.max(0, max - 1))}…`
}

export function getConversationId(pathname: string): string | null {
const match = pathname.match(/^\/(?:a\/)?(?:c|chat)\/([^/?#]+)/)
const id = match?.[1]
if (!id || id === 'c' || id === 'chat')
return null
try {
return decodeURIComponent(id)
}
catch {
return id
}
}

function titleFromDocument(): string | null {
const raw = document.title
.replace(/\s*[—–|·-]\s*Grok\s*$/i, '')
.replace(/^Grok\s*[—–|·-]\s*/i, '')
.trim()
if (!raw || /^grok$/i.test(raw))
return null
return raw
}

function titleFromSidebar(id: string): string | null {
const selectors = [
`a[href="/c/${CSS.escape(id)}"]`,
`a[href="/chat/${CSS.escape(id)}"]`,
]
for (const selector of selectors) {
const text = document.querySelector(selector)?.textContent
if (text && collapse(text))
return collapse(text)
}

for (const link of document.querySelectorAll<HTMLAnchorElement>('a[href^="/c/"], a[href^="/chat/"]')) {
if (link.href.includes(id) && link.textContent)
return collapse(link.textContent)
}
return null
}

function lastPromptFromDom(): string | null {
const bubbles = document.querySelectorAll(
'.message-bubble.user-message, .user-message .message-bubble, .user-message',
)
for (let i = bubbles.length - 1; i >= 0; i--) {
const clone = bubbles[i]!.cloneNode(true) as HTMLElement
for (const noise of clone.querySelectorAll('button, svg, nav, time, [aria-hidden="true"]'))
noise.remove()
const text = collapse(clone.textContent ?? '')
if (text)
return text
}
return null
}

export function isGenerating(): boolean {
if (document.querySelector('[id="model-select-trigger"]') && document.querySelector('.animate-gaussian'))
return true
if (document.querySelector('.animate-gaussian'))
return true
const abort = document.querySelector(
'[aria-label="Stop model response"], [aria-label*="Stop model"], [aria-label*="Stop generating"]',
)
return !!abort
}

async function fetchConversationFallback(id: string): Promise<void> {
if (cache?.id === id && Date.now() - cache.fetchedAt < FETCH_INTERVAL)
return
if (inflight)
return inflight

inflight = (async () => {
const next: ConversationCache = {
id,
title: cache?.id === id ? cache.title : null,
lastPrompt: cache?.id === id ? cache.lastPrompt : null,
fetchedAt: Date.now(),
}

try {
const detail = await fetch(
`https://grok.com/rest/app-chat/conversations_v2/${encodeURIComponent(id)}`,
{ credentials: 'include', headers: { Accept: 'application/json' } },
)
if (detail.ok) {
const payload = await detail.json() as {
conversation?: { title?: string }
}
if (payload.conversation?.title)
next.title = collapse(payload.conversation.title)
}
}
catch {
// Title API is optional; DOM title is used when this fails.
}

try {
const nodesResponse = await fetch(
`https://grok.com/rest/app-chat/conversations/${encodeURIComponent(id)}/response-node`,
{ credentials: 'include', headers: { Accept: 'application/json' } },
)
if (nodesResponse.ok) {
const nodesPayload = await nodesResponse.json() as {
responseNodes?: Array<{ responseId?: string, sender?: string }>
}
const responseIds = (nodesPayload.responseNodes ?? [])
.map(node => node.responseId)
.filter((value): value is string => !!value)
.slice(-12)

if (responseIds.length) {
const loaded = await fetch(
`https://grok.com/rest/app-chat/conversations/${encodeURIComponent(id)}/load-responses`,
{
method: 'POST',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ responseIds }),
},
)
if (loaded.ok) {
const loadedPayload = await loaded.json() as {
responses?: Array<{ sender?: string, message?: string }>
}
const human = (loadedPayload.responses ?? [])
.filter(response => /^(?:human|user)$/i.test(response.sender ?? ''))
.at(-1)
if (human?.message)
next.lastPrompt = collapse(human.message)
}
}
}
}
catch {
// Prompt API is optional; DOM messages are used when this fails.
}

cache = next
})().finally(() => {
inflight = null
})

return inflight
}

export async function getConversationInfo(pathname: string): Promise<ConversationInfo> {
const id = getConversationId(pathname)
if (!id) {
cache = null
return {
id: null,
title: titleFromDocument(),
lastPrompt: lastPromptFromDom(),
isGenerating: isGenerating(),
}
}

const title = titleFromSidebar(id) ?? titleFromDocument()
const lastPrompt = lastPromptFromDom()

if (!title || !lastPrompt)
await fetchConversationFallback(id)

return {
id,
title: title ?? (cache?.id === id ? cache.title : null),
lastPrompt: lastPrompt ?? (cache?.id === id ? cache.lastPrompt : null),
isGenerating: isGenerating(),
}
}

export function formatPrompt(prompt: string): string {
return truncate(prompt, 96)
}

export function formatTitle(title: string): string {
return truncate(title, 128)
}
Loading
Loading