package main import ( "log" "os" "regexp" "strings" "time" ) var fileIDRegexp = regexp.MustCompile(`(file_[0-9a-fA-F]{32}|file-[a-zA-Z0-9_-]{24,36})`) func sendChat(prompt string) (string, string, error) { return sendChatWithConversation(prompt, "", cfg.DefaultModel, cfg.DefaultThinkingEffort) } func sendChatWithConversation(prompt, conversationID, model, thinkingEffort string) (string, string, error) { return sendChatWithConversationAndAttachments(prompt, conversationID, model, thinkingEffort, nil) } func sendChatWithConversationAndAttachments(prompt, conversationID, model, thinkingEffort string, attachments []FileAttachment) (string, string, error) { var response, rawText, newConvID string var err error // ─── Try 1: Direct Go HTTP call using synced cookies (fastest, no extension needed) ─── if HasCookies() && len(attachments) == 0 { log.Println("[chat] Attempting direct API call using synced cookies...") response, rawText, newConvID, err = SendChatDirect(prompt, conversationID, model, thinkingEffort) if err == nil { log.Println("[chat] ✅ Direct API call succeeded!") return processResponse(response, rawText, newConvID, prompt, attachments) } log.Printf("[chat] ⚠️ Direct API call failed: %v — falling back to extension", err) } // ─── Try 2: Extension full_conversation method (browser-based) ─── log.Println("[chat] Using extension full_conversation method...") response, rawText, newConvID, err = sendChatViaFullConversation(prompt, conversationID, model, thinkingEffort, attachments...) if err != nil { return "", "", err } return processResponse(response, rawText, newConvID, prompt, attachments) } // processResponse handles post-processing: auto-download images, poll for async images func processResponse(response, rawText, newConvID, prompt string, attachments []FileAttachment) (string, string, error) { // Auto-download any files found in the rawText or response var rawFileIDs []string rawFileIDs = append(rawFileIDs, fileIDRegexp.FindAllString(rawText, -1)...) rawFileIDs = append(rawFileIDs, fileIDRegexp.FindAllString(response, -1)...) // Dedup and filter file IDs (excluding any attachment IDs we uploaded) uploadedIDs := make(map[string]bool) for _, att := range attachments { uploadedIDs[att.ID] = true } seen := make(map[string]bool) var fileIDs []string for _, id := range rawFileIDs { if !seen[id] && !uploadedIDs[id] { seen[id] = true fileIDs = append(fileIDs, id) } } log.Printf("[chat] Found %d new file ID(s) to download: %v", len(fileIDs), fileIDs) if len(fileIDs) > 0 { for _, id := range fileIDs { registerFilePrompt(id, prompt) name := getPromptFilename(prompt, id) log.Printf("[auto-download] Detected image %s (%s) in response, downloading...", id, name) // Retry download up to 10 times because the image might still be generating/saving on OpenAI backend var data []byte var err error for attempt := 1; attempt <= 10; attempt++ { data, err = downloadChatGPTFile(id) if err == nil { break } log.Printf("[auto-download] Image %s not ready yet, retrying in 2 seconds... (attempt %d/10)", id, attempt) time.Sleep(2 * time.Second) } if err == nil { _ = os.MkdirAll("output", 0755) localPath := "output/" + name + ".png" err = os.WriteFile(localPath, data, 0644) if err != nil { log.Printf("[auto-download] Error saving file %s: %v", localPath, err) } else { log.Printf("[auto-download] Successfully saved image to %s", localPath) } } else { log.Printf("[auto-download] Error downloading image %s: %v", id, err) } } } shouldPoll := newConvID != "" && (strings.Contains(response, "Processing image") || strings.Contains(response, "creating images") || strings.Contains(response, "generating your image") || len(attachments) > 0 || isPromptForImage(prompt)) if shouldPoll { log.Printf("[chat] detected async image generation/edit in conversation %s, starting poll...", newConvID) var excludeIDs []string for _, att := range attachments { excludeIDs = append(excludeIDs, att.ID) } polledResponse, pollErr := pollForImage(prompt, newConvID, 150*time.Second, excludeIDs...) if pollErr == nil { return polledResponse, newConvID, nil } log.Printf("[chat] image poll failed: %v, returning original response", pollErr) } return response, newConvID, nil } func isPromptForImage(prompt string) bool { p := strings.ToLower(prompt) keywords := []string{ "generate", "create", "draw", "make", "edit", "add", "remove", "change", "modify", "paint", "cartoon", "3d", "render", "picture", "photo", "illustration", "dall-e", "dalle", } for _, kw := range keywords { if strings.Contains(p, kw) { return true } } return false }