Claude Fork Conversation

Adds forking functionality to claude.ai

  1. // ==UserScript==
  2. // @name Claude Fork Conversation
  3. // @namespace https://lugia19.com
  4. // @version 0.5.3
  5. // @description Adds forking functionality to claude.ai
  6. // @match https://claude.ai/*
  7. // @grant none
  8. // @license GPLv3
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. 'use strict';
  13. let pendingForkModel = null;
  14. let includeAttachments = true;
  15. let isProcessing = false;
  16. let pendingUseSummary = false;
  17.  
  18. //#region UI elements creation
  19. function createBranchButton() {
  20. const button = document.createElement('button');
  21. button.className = 'branch-button flex flex-row items-center gap-1 rounded-md p-1 py-0.5 text-xs transition-opacity delay-100 hover:bg-bg-200 group/button';
  22.  
  23. button.innerHTML = `
  24. <svg xmlns="http://www.w3.org/2000/svg" width="1.35em" height="1.35em" fill="currentColor" viewBox="0 0 22 22">
  25. <path d="M7 5C7 3.89543 7.89543 3 9 3C10.1046 3 11 3.89543 11 5C11 5.74028 10.5978 6.38663 10 6.73244V14.0396H11.7915C12.8961 14.0396 13.7915 13.1441 13.7915 12.0396V10.7838C13.1823 10.4411 12.7708 9.78837 12.7708 9.03955C12.7708 7.93498 13.6662 7.03955 14.7708 7.03955C15.8753 7.03955 16.7708 7.93498 16.7708 9.03955C16.7708 9.77123 16.3778 10.4111 15.7915 10.7598V12.0396C15.7915 14.2487 14.0006 16.0396 11.7915 16.0396H10V17.2676C10.5978 17.6134 11 18.2597 11 19C11 20.1046 10.1046 21 9 21C7.89543 21 7 20.1046 7 19C7 18.2597 7.4022 17.6134 8 17.2676V6.73244C7.4022 6.38663 7 5.74028 7 5Z"/>
  26. </svg>
  27. <span>Fork</span>
  28. `;
  29.  
  30. button.onclick = async (e) => {
  31. e.preventDefault();
  32. e.stopPropagation();
  33.  
  34. const modal = await createModal();
  35. document.body.appendChild(modal);
  36.  
  37. // Add event listeners
  38. modal.querySelector('#cancelFork').onclick = () => {
  39. modal.remove();
  40. };
  41.  
  42. // And in our modal click handler:
  43. modal.querySelector('#confirmFork').onclick = async () => {
  44. const model = modal.querySelector('select').value;
  45. const useSummary = modal.querySelector('#summaryMode').checked;
  46.  
  47. // Disable the button to prevent multiple clicks
  48. const confirmBtn = modal.querySelector('#confirmFork');
  49. confirmBtn.disabled = true;
  50. confirmBtn.textContent = 'Processing...';
  51.  
  52. await forkConversationClicked(model, button, modal, useSummary);
  53. modal.remove();
  54. };
  55.  
  56. // Click outside to cancel
  57. modal.onclick = (e) => {
  58. if (e.target === modal) {
  59. modal.remove();
  60. }
  61. };
  62. };
  63.  
  64. return button;
  65. }
  66.  
  67. async function createModal() {
  68. const modal = document.createElement('div');
  69. modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
  70.  
  71. modal.innerHTML = `
  72. <div class="bg-bg-100 rounded-lg p-6 shadow-xl max-w-sm w-full mx-4 border border-border-300">
  73. <h3 class="text-lg font-semibold mb-4 text-text-100">Choose Model for Fork</h3>
  74. <select class="w-full p-2 rounded mb-4 bg-bg-200 text-text-100 border border-border-300">
  75. <option value="claude-sonnet-4-20250514">Sonnet 4</option>
  76. <option value="claude-opus-4-20250514">Opus 4</option>
  77. <option value="claude-3-7-sonnet-20250219">Sonnet 3.7</option>
  78. <option value="claude-3-opus-20240229">Opus 3</option>
  79. <option value="claude-3-5-haiku-20241022">Haiku 3.5</option>
  80. </select>
  81. <div class="mb-4 space-y-2">
  82. <div class="flex items-center justify-between mb-3 p-2 bg-bg-200 rounded">
  83. <span class="text-text-100 font-medium">Fork Type:</span>
  84. <div class="flex items-center gap-4">
  85. <label class="flex items-center space-x-2">
  86. <input type="radio" id="fullChatlog" name="forkType" value="full" checked class="accent-accent-main-100">
  87. <span class="text-text-100">Full Chatlog</span>
  88. </label>
  89. <label class="flex items-center space-x-2">
  90. <input type="radio" id="summaryMode" name="forkType" value="summary" class="accent-accent-main-100">
  91. <span class="text-text-100">Summary</span>
  92. </label>
  93. </div>
  94. </div>
  95. <label class="flex items-center space-x-2">
  96. <input type="checkbox" id="includeFiles" class="rounded border-border-300" checked>
  97. <span class="text-text-100">Include files</span>
  98. </label>
  99. </div>
  100. <p class="text-sm text-text-400 sm:text-[0.75rem]">Note: Should you choose a slow model such as Opus, you may need to wait and refresh the page for the response to appear.</p>
  101. <div class="mt-4 flex flex-col gap-2 sm:flex-row-reverse">
  102. <button class="inline-flex items-center justify-center relative shrink-0 ring-offset-2 ring-offset-bg-300 ring-accent-main-100 focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none disabled:drop-shadow-none bg-accent-main-100 bg-gradient-to-r from-accent-main-100 via-accent-main-200/50 to-accent-main-200 bg-[length:200%_100%] hover:bg-right active:bg-accent-main-000 border-0.5 border-border-300 text-oncolor-100 font-medium font-styrene drop-shadow-sm transition-all shadow-[inset_0_0.5px_0px_rgba(255,255,0,0.15)] [text-shadow:_0_1px_2px_rgb(0_0_0_/_10%)] active:shadow-[inset_0_1px_6px_rgba(0,0,0,0.2)] hover:from-accent-main-200 hover:to-accent-main-200 h-9 px-4 py-2 rounded-lg min-w-[5rem] active:scale-[0.985] whitespace-nowrap" id="confirmFork">
  103. Fork Chat
  104. </button>
  105. <button class="inline-flex items-center justify-center relative shrink-0 ring-offset-2 ring-offset-bg-300 ring-accent-main-100 focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none disabled:drop-shadow-none bg-[radial-gradient(ellipse,_var(--tw-gradient-stops))] from-bg-500/10 from-50% to-bg-500/30 border-0.5 border-border-400 font-medium font-styrene text-text-100/90 transition-colors active:bg-bg-500/50 hover:text-text-000 hover:bg-bg-500/60 h-9 px-4 py-2 rounded-lg min-w-[5rem] active:scale-[0.985] whitespace-nowrap" id="cancelFork">
  106. Cancel
  107. </button>
  108. </div>
  109. </div>
  110. `;
  111.  
  112. try {
  113. const accountData = await fetchAccountSettings();
  114. originalSettings = accountData.settings;
  115. } catch (error) {
  116. console.error('Failed to fetch account settings:', error);
  117. }
  118.  
  119. return modal;
  120. }
  121.  
  122.  
  123. function findMessageControls(messageElement) {
  124. if (messageElement.classList.contains('font-user-message')) {
  125. const group = messageElement.closest('.group');
  126. const buttons = group?.querySelectorAll('button');
  127. if (!buttons) return;
  128. const editButton = Array.from(buttons).find(button =>
  129. button.textContent.includes('Edit')
  130. );
  131. return editButton?.closest('.justify-between');
  132. }
  133.  
  134. if (messageElement.classList.contains('font-claude-message')) {
  135. const group = messageElement.closest('.group');
  136. const buttons = group?.querySelectorAll('button');
  137. const retryButton = Array.from(buttons).find(button =>
  138. button.textContent.includes('Retry')
  139. );
  140. return retryButton?.closest('.justify-between');
  141. }
  142.  
  143. return null;
  144. }
  145.  
  146. function addBranchButtons() {
  147. if (isProcessing) return;
  148. try {
  149. isProcessing = true;
  150. const messages = document.querySelectorAll('.font-claude-message');
  151. messages.forEach((message) => {
  152. const controls = findMessageControls(message);
  153. if (controls && !controls.querySelector('.branch-button')) {
  154. const container = document.createElement('div');
  155. container.className = 'flex items-center gap-0.5';
  156. const divider = document.createElement('div');
  157. divider.className = 'w-px h-4/5 self-center bg-border-300 mr-0.5';
  158. const branchBtn = createBranchButton();
  159. container.appendChild(branchBtn);
  160. container.appendChild(divider);
  161. controls.insertBefore(container, controls.firstChild);
  162. }
  163. });
  164. } catch (error) {
  165. console.error('Error adding branch buttons:', error);
  166. } finally {
  167. isProcessing = false;
  168. }
  169. }
  170.  
  171. //#endregion
  172.  
  173. async function forkConversationClicked(model, forkButton, modal, useSummary = false) {
  174. // Get conversation ID from URL
  175. const conversationId = window.location.pathname.split('/').pop();
  176. console.log('Forking conversation', conversationId, 'with model', model);
  177.  
  178. if (originalSettings) {
  179. const newSettings = { ...originalSettings };
  180. newSettings.paprika_mode = null; // Ensure it's off when we create the conversation (will be overridden to on if needed)
  181. console.log('Updating settings:', newSettings);
  182. await updateAccountSettings(newSettings);
  183. }
  184.  
  185. // Set up our global to catch the next retry request
  186. pendingForkModel = model;
  187. includeAttachments = modal.querySelector('#includeFiles')?.checked ?? true;
  188. pendingUseSummary = useSummary;
  189.  
  190. // Find and click the retry button in the same control group as our fork button
  191. const buttonGroup = forkButton.closest('.justify-between');
  192. const retryButton = Array.from(buttonGroup.querySelectorAll('button'))
  193. .find(button => button.textContent.includes('Retry'));
  194.  
  195. if (retryButton) {
  196. // Dispatch pointerdown event which Radix UI components use
  197. retryButton.dispatchEvent(new PointerEvent('pointerdown', {
  198. bubbles: true,
  199. cancelable: true,
  200. view: window,
  201. pointerType: 'mouse'
  202. }));
  203.  
  204. // Wait for the dropdown to appear
  205. await new Promise(resolve => setTimeout(resolve, 300));
  206.  
  207. // Look for the dropdown menu with "With no changes" option
  208. const withNoChangesOption = Array.from(document.querySelectorAll('[role="menuitem"]'))
  209. .find(element => element.textContent.includes('With no changes'));
  210.  
  211. if (withNoChangesOption) {
  212. console.log('Detected retry dropdown, clicking "With no changes"');
  213. // For the menu item, a regular click should work
  214. withNoChangesOption.click();
  215. } else {
  216. console.log('No dropdown detected, assuming direct retry');
  217. retryButton.click();
  218. // If no dropdown appeared, the retry might have been triggered directly
  219. }
  220. } else {
  221. console.error('Could not find retry button');
  222. }
  223. }
  224.  
  225. //#region Convo extraction & Other API
  226.  
  227. let originalSettings = null;
  228.  
  229. async function fetchAccountSettings() {
  230. const response = await fetch('/api/account');
  231. console.log('Account settings response:', response);
  232. const data = await response.json();
  233. return data;
  234. }
  235.  
  236. async function updateAccountSettings(settings) {
  237. await fetch('/api/account', {
  238. method: 'PUT',
  239. headers: {
  240. 'Content-Type': 'application/json'
  241. },
  242. body: JSON.stringify({ settings })
  243. });
  244. }
  245.  
  246. async function getConversationContext(orgId, conversationId, targetParentUuid) {
  247. const response = await fetch(`/api/organizations/${orgId}/chat_conversations/${conversationId}?tree=False&rendering_mode=messages&render_all_tools=true`);
  248. const conversationData = await response.json();
  249.  
  250. let messages = [];
  251. let projectUuid = conversationData?.project?.uuid || null;
  252. const chatName = conversationData.name;
  253. const files = []
  254. const syncsources = []
  255. const attachments = []
  256.  
  257. for (const message of conversationData.chat_messages) {
  258. let messageContent = [];
  259.  
  260. // Process content array
  261. for (const content of message.content) {
  262. if (content.text) {
  263. messageContent.push(content.text);
  264. }
  265. if (content.input?.code) {
  266. messageContent.push(content.input.code);
  267. }
  268. if (content.content?.text) {
  269. messageContent.push(content.content.text);
  270. }
  271. }
  272.  
  273. // Process files with download URLs
  274. if (message.files_v2) {
  275. for (const file of message.files_v2) {
  276. let fileUrl;
  277. if (file.file_kind === "image") {
  278. fileUrl = file.preview_asset.url;
  279. } else if (file.file_kind === "document") {
  280. fileUrl = file.document_asset.url;
  281. }
  282.  
  283. if (fileUrl) {
  284. files.push({
  285. uuid: file.file_uuid,
  286. url: fileUrl,
  287. kind: file.file_kind,
  288. name: file.file_name
  289. });
  290. }
  291. }
  292. }
  293.  
  294. // Add attachment objects
  295. if (message.attachments) {
  296. for (const attachment of message.attachments) {
  297. attachments.push(attachment);
  298. }
  299. }
  300.  
  301. // Process sync sources
  302. for (const sync of message.sync_sources) {
  303. syncsources.push(sync);
  304. }
  305.  
  306. messages.push(messageContent.join(' '));
  307.  
  308. // Process until we find a message that has our target UUID as parent
  309. if (message.parent_message_uuid === targetParentUuid) {
  310. break;
  311. }
  312. }
  313.  
  314. if (!includeAttachments) {
  315. return {
  316. chatName,
  317. messages,
  318. syncsources: [],
  319. attachments: [],
  320. files: [],
  321. projectUuid
  322. };
  323. }
  324.  
  325. return {
  326. chatName,
  327. messages,
  328. syncsources,
  329. attachments,
  330. files,
  331. projectUuid
  332. };
  333. }
  334.  
  335. //#region File handlers (download, upload, sync)
  336. async function downloadFiles(files) {
  337. const downloadedFiles = [];
  338.  
  339. for (const file of files) {
  340. try {
  341. const response = await fetch(file.url);
  342. const blob = await response.blob();
  343.  
  344. downloadedFiles.push({
  345. data: blob,
  346. name: file.name,
  347. kind: file.kind,
  348. originalUuid: file.uuid
  349. });
  350. } catch (error) {
  351. console.error(`Failed to download file ${file.name}:`, error);
  352. }
  353. }
  354.  
  355. return downloadedFiles;
  356. }
  357.  
  358. async function uploadFile(orgId, file) {
  359. const formData = new FormData();
  360. formData.append('file', file.data, file.name);
  361.  
  362. const response = await fetch(`/api/${orgId}/upload`, {
  363. method: 'POST',
  364. body: formData
  365. });
  366.  
  367. const uploadResult = await response.json();
  368. return uploadResult.file_uuid;
  369. }
  370.  
  371. async function processSyncSource(orgId, syncsource) {
  372. const response = await fetch(`/api/organizations/${orgId}/sync/chat`, {
  373. method: 'POST',
  374. headers: {
  375. 'Content-Type': 'application/json'
  376. },
  377. body: JSON.stringify({
  378. sync_source_config: syncsource?.config,
  379. sync_source_type: syncsource?.type
  380. })
  381. })
  382.  
  383. if (!response.ok) {
  384. console.error(`Failed to process sync source: ${response.statusText}`);
  385. return null;
  386. }
  387. const result = await response.json();
  388. return result.uuid;
  389. }
  390. //#endregion
  391.  
  392. //#region Convo forking
  393. function generateUuid() {
  394. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
  395. const r = Math.random() * 16 | 0;
  396. const v = c === 'x' ? r : (r & 0x3 | 0x8);
  397. return v.toString(16);
  398. });
  399. }
  400.  
  401.  
  402. // 1. Create a standalone conversation creation function
  403. async function createConversation(orgId, name, model = null, projectUuid = null, thinking = false) {
  404. const newUuid = generateUuid();
  405. const bodyJSON = {
  406. uuid: newUuid,
  407. name: name,
  408. include_conversation_preferences: true,
  409. project_uuid: projectUuid,
  410. }
  411.  
  412. if (model) bodyJSON.model = model;
  413.  
  414. if (thinking) {
  415. let isFree = true;
  416. const statSigResponse = await fetch(`/api/bootstrap/${orgId}/statsig`, {
  417. method: 'GET',
  418. headers: {
  419. 'Content-Type': 'application/json'
  420. },
  421. });
  422. if (statSigResponse.ok) {
  423. const statSigData = await statSigResponse.json();
  424. if (statSigData?.user?.custom?.orgType !== 'claude_free') {
  425. isFree = false;
  426. }
  427. }
  428.  
  429. if (!isFree) {
  430. bodyJSON.paprika_mode = "extended";
  431. }
  432.  
  433. }
  434. const createResponse = await fetch(`/api/organizations/${orgId}/chat_conversations`, {
  435. method: 'POST',
  436. headers: {
  437. 'Content-Type': 'application/json'
  438. },
  439. body: JSON.stringify(bodyJSON)
  440. });
  441.  
  442. if (!createResponse.ok) {
  443. throw new Error('Failed to create conversation');
  444. }
  445.  
  446. return newUuid;
  447. }
  448.  
  449. async function createForkedConversation(orgId, context, model, styleData) {
  450. const newName = `Fork of ${context.chatName}`;
  451.  
  452. // Create a new chat conversation
  453. const newUuid = await createConversation(orgId, newName, model, context.projectUuid);
  454.  
  455. // Create the chatlog
  456. if (context.messages) {
  457. const chatlog = context.messages.map((msg, index) => {
  458. const role = index % 2 === 0 ? 'User' : 'Assistant';
  459. return `${role}\n${msg}`;
  460. }).join('\n\n');
  461.  
  462. context.attachments.push({
  463. "extracted_content": chatlog,
  464. "file_name": "chatlog.txt",
  465. "file_size": 0,
  466. "file_type": "text/plain"
  467. });
  468. }
  469.  
  470. const message = context.messages
  471. ? "This conversation is forked from the attached chatlog.txt\nYou are Assistant. Simply say 'Acknowledged' and wait for user input."
  472. : "This conversation is forked based on the summary in conversation_summary.txt\nYou are Assistant. Simply say 'Acknowledged' and wait for user input.";
  473.  
  474. // Send initial message to set up conversation history
  475. const completionResponse = await fetch(`/api/organizations/${orgId}/chat_conversations/${newUuid}/completion`, {
  476. method: 'POST',
  477. headers: {
  478. 'Content-Type': 'application/json'
  479. },
  480. body: JSON.stringify({
  481. prompt: message,
  482. model: model,
  483. parent_message_uuid: '00000000-0000-4000-8000-000000000000',
  484. attachments: context.attachments,
  485. files: context.files,
  486. sync_sources: context.syncsources,
  487. personalized_styles: styleData
  488. })
  489. });
  490.  
  491. if (!completionResponse.ok) {
  492. throw new Error('Failed to initialize conversation');
  493. }
  494.  
  495. // Sleep for 2 seconds to allow the response to be fully created
  496. await new Promise(r => setTimeout(r, 2000));
  497. return newUuid;
  498. }
  499.  
  500. async function generateSummary(orgId, context) {
  501. // Create a temporary conversation for summarization
  502. const summaryConvoName = `Temp_Summary_${Date.now()}`;
  503. const summaryConvoId = await createConversation(orgId, summaryConvoName, null, context.projectUuid, true);
  504.  
  505. try {
  506. // Create the chatlog
  507. const chatlog = context.messages.map((msg, index) => {
  508. const role = index % 2 === 0 ? 'User' : 'Assistant';
  509. return `${role}\n${msg}`;
  510. }).join('\n\n');
  511.  
  512. const summaryAttachments = [...context.attachments, {
  513. "extracted_content": chatlog,
  514. "file_name": "chatlog.txt",
  515. "file_size": 0,
  516. "file_type": "text/plain"
  517. }];
  518.  
  519. // Ask the model to create a summary
  520. const summaryPrompt = "I've attached a chatlog from a previous conversation. Please create a complete, detailed summary of the conversation that covers all important points, questions, and responses. This summary will be used to continue the conversation in a new chat, so make sure it provides enough context to understand the full discussion. Be through, and think things through. Don't include any information already present in the other attachments, as those will be forwarded to the new chat as well.";
  521.  
  522. const summaryResponse = await fetch(`/api/organizations/${orgId}/chat_conversations/${summaryConvoId}/completion`, {
  523. method: 'POST',
  524. headers: {
  525. 'Content-Type': 'application/json'
  526. },
  527. body: JSON.stringify({
  528. prompt: summaryPrompt,
  529. parent_message_uuid: '00000000-0000-4000-8000-000000000000',
  530. attachments: summaryAttachments,
  531. files: [],
  532. sync_sources: []
  533. })
  534. });
  535.  
  536. if (!summaryResponse.ok) {
  537. console.error('Failed to generate summary');
  538. return null;
  539. }
  540.  
  541.  
  542. // Implement polling with timeout for assistant response
  543. const maxRetries = 6; // 6 retries * 5 seconds = 30 seconds max
  544. let assistantMessage = null;
  545.  
  546. for (let attempt = 0; attempt < maxRetries; attempt++) {
  547. // Wait 5 seconds between attempts
  548. await new Promise(r => setTimeout(r, 5000));
  549.  
  550. console.log(`Checking for summary response (attempt ${attempt + 1}/${maxRetries})...`);
  551.  
  552. // Fetch conversation to check for assistant response
  553. const convoResponse = await fetch(`/api/organizations/${orgId}/chat_conversations/${summaryConvoId}?tree=False&rendering_mode=messages&render_all_tools=true`);
  554. const convoData = await convoResponse.json();
  555.  
  556. // Find the assistant's response
  557. assistantMessage = convoData.chat_messages.find(msg => msg.sender === 'assistant');
  558.  
  559. if (assistantMessage) {
  560. console.log('Found assistant summary response');
  561. break;
  562. }
  563.  
  564. if (attempt === maxRetries - 1) {
  565. console.error('Could not find assistant summary response after maximum retries');
  566. throw new Error('Could not find assistant summary response after 30 seconds');
  567. }
  568. }
  569.  
  570. // Extract the text of the summary
  571. let summaryText = '';
  572. for (const content of assistantMessage.content) {
  573. if (content.text) {
  574. summaryText += content.text;
  575. }
  576. if (content.content?.text) {
  577. summaryText += content.text;
  578. }
  579. }
  580.  
  581. return summaryText;
  582. } finally {
  583. // Delete the temporary summarization conversation
  584. try {
  585. await fetch(`/api/organizations/${orgId}/chat_conversations/${summaryConvoId}`, {
  586. method: 'DELETE'
  587. });
  588. } catch (error) {
  589. console.error('Failed to delete temporary summary conversation:', error);
  590. }
  591. }
  592. }
  593. //#endregion
  594.  
  595. //#region Fetch patching
  596. const originalFetch = window.fetch;
  597. window.fetch = async (...args) => {
  598. const [input, config] = args;
  599.  
  600. // Get the URL string whether it's a string or Request object
  601.  
  602. let url = undefined
  603. if (input instanceof URL) {
  604. url = input.href
  605. } else if (typeof input === 'string') {
  606. url = input
  607. } else if (input instanceof Request) {
  608. url = input.url
  609. }
  610.  
  611. // In the fetch patching section
  612. if (url && url.includes('/retry_completion') && pendingForkModel) {
  613. console.log('Intercepted retry request:', config?.body);
  614. const bodyJSON = JSON.parse(config?.body);
  615. const messageID = bodyJSON?.parent_message_uuid;
  616. const urlParts = url.split('/');
  617. const orgId = urlParts[urlParts.indexOf('organizations') + 1];
  618. const conversationId = urlParts[urlParts.indexOf('chat_conversations') + 1];
  619.  
  620. let styleData = bodyJSON?.personalized_styles;
  621.  
  622. try {
  623. // Get conversation context
  624. console.log('Getting conversation context, includeAttachments:', includeAttachments);
  625. const context = await getConversationContext(orgId, conversationId, messageID);
  626.  
  627. // Process files and sync sources if needed
  628. if (includeAttachments) {
  629. const downloadedFiles = await downloadFiles(context.files);
  630.  
  631. // Parallel processing of files and syncs
  632. [context.files, context.syncsources] = await Promise.all([
  633. Promise.all(downloadedFiles.map(file => uploadFile(orgId, file))),
  634. Promise.all(context.syncsources.map(syncsource => processSyncSource(orgId, syncsource)))
  635. ]);
  636. } else {
  637. context.files = [];
  638. context.syncsources = [];
  639. }
  640.  
  641. let newConversationId;
  642.  
  643. if (pendingUseSummary) {
  644. // Generate summary
  645. console.log("Generating summary for forking");
  646. const summary = await generateSummary(orgId, context, pendingForkModel);
  647. if (summary === null) {
  648. // Fall back to normal forking
  649. newConversationId = await createForkedConversation(orgId, context, pendingForkModel, styleData);
  650. } else {
  651. // Prepare context for summary-based fork
  652. const summaryContext = { ...context };
  653. summaryContext.messages = null; // Don't generate chatlog
  654. summaryContext.attachments = [{
  655. "extracted_content": summary,
  656. "file_name": "conversation_summary.txt",
  657. "file_size": 0,
  658. "file_type": "text/plain"
  659. }];
  660. // Create forked conversation with summary
  661. newConversationId = await createForkedConversation(orgId, summaryContext, pendingForkModel, styleData);
  662. }
  663. } else {
  664. // Standard workflow with full chatlog
  665. newConversationId = await createForkedConversation(orgId, context, pendingForkModel, styleData);
  666. }
  667.  
  668. // Restore original settings
  669. if (originalSettings) {
  670. await updateAccountSettings(originalSettings);
  671. }
  672.  
  673. // Navigate to new conversation
  674. console.log('Forked conversation created:', newConversationId);
  675. window.location.href = `/chat/${newConversationId}`;
  676.  
  677. } catch (error) {
  678. console.error('Failed to fork conversation:', error);
  679. // Restore original settings even if forking fails
  680. if (originalSettings) {
  681. await updateAccountSettings(originalSettings);
  682. }
  683. }
  684.  
  685. originalSettings = null;
  686. pendingForkModel = null; // Clear the pending flag
  687. pendingUseSummary = false; // Clear the summary flag
  688. return new Response(JSON.stringify({ success: true }));
  689. }
  690.  
  691. return originalFetch(...args);
  692. };
  693. //#endregion
  694.  
  695. //Check for buttons every 3 seconds
  696. setInterval(addBranchButtons, 3000);
  697. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址