From 4f2b901ba963d43529b287d913d983c471aab769 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Mar 2026 09:32:50 +0000 Subject: [PATCH 1/2] Add diff display, API key validation, and empty states - Add patch_text column to review_sections (new migration) - Store file patch_text from harness pipeline in ReviewJob - Serve patch_text in controller JSON response - Rewrite DiffPanel to render actual diff lines with +/- coloring - Add API key validation in create action with clear error message - Add empty states: idle prompt, loading spinners, failed message, no-files-flagged state, in-progress indicator for partial reviews https://claude.ai/code/session_01W4XaE7NTEUQQTeRSNjKnE3 --- app/controllers/reviews_controller.rb | 9 +++- .../components/review/DiffPanel.jsx | 49 +++++++++++++++---- .../components/review/ReviewApp.jsx | 17 ++++++- .../components/review/ReviewPanel.jsx | 19 +++++-- app/jobs/review_job.rb | 1 + ...20000_add_patch_text_to_review_sections.rb | 5 ++ 6 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 db/migrate/20260311120000_add_patch_text_to_review_sections.rb diff --git a/app/controllers/reviews_controller.rb b/app/controllers/reviews_controller.rb index b8bdc1c..f71fa22 100644 --- a/app/controllers/reviews_controller.rb +++ b/app/controllers/reviews_controller.rb @@ -3,6 +3,11 @@ def index end def create + unless Harness.configuration&.api_key.present? + return render json: { error: "ANTHROPIC_API_KEY is not configured on the server." }, + status: :service_unavailable + end + review = Review.create!(pr_url: params[:pr_url], status: Review::PENDING) ReviewJob.perform_later(review.id) render json: { id: review.id, status: review.status } @@ -51,8 +56,8 @@ def review_json(review) { id: s.id, filename: s.filename, language: s.language, priority: s.priority, walkthrough: s.walkthrough, - findings: s.findings, human_comments: s.human_comments, - status: s.status + patch_text: s.patch_text, findings: s.findings, + human_comments: s.human_comments, status: s.status } end } diff --git a/app/javascript/components/review/DiffPanel.jsx b/app/javascript/components/review/DiffPanel.jsx index 909372e..0905d1e 100644 --- a/app/javascript/components/review/DiffPanel.jsx +++ b/app/javascript/components/review/DiffPanel.jsx @@ -1,6 +1,13 @@ import React, { useState, useRef, useEffect } from 'react' -const DiffPanel = ({ sections, highlightFile, highlightRange }) => { +const lineStyle = (line) => { + if (line.startsWith('+')) return 'bg-green-50 text-green-800' + if (line.startsWith('-')) return 'bg-red-50 text-red-800' + if (line.startsWith('@@')) return 'bg-blue-50 text-blue-600 font-medium' + return 'text-gray-700' +} + +const DiffPanel = ({ sections, status, highlightFile, highlightRange }) => { const [collapsed, setCollapsed] = useState({}) const fileRefs = useRef({}) @@ -15,30 +22,52 @@ const DiffPanel = ({ sections, highlightFile, highlightRange }) => { } if (!sections || sections.length === 0) { - return
No diff data available
+ if (status === 'reviewing' || status === 'submitting') { + return ( +
+ + Fetching diff... +
+ ) + } + return ( +
+ Submit a PR URL to see the diff here. +
+ ) } return ( -
+
{sections.map((section) => (
fileRefs.current[section.filename] = el} - className="border border-gray-200 rounded" + className="border border-gray-200 rounded overflow-hidden" > - {!collapsed[section.filename] && section.findings && ( -
- {section.findings.length} finding{section.findings.length !== 1 ? 's' : ''} - {section.findings.some(f => f.severity === 'red_flag') && ( - contains red flags + {!collapsed[section.filename] && ( +
+ {section.patch_text ? ( +
+                  {section.patch_text.split('\n').map((line, i) => (
+                    
{line || ' '}
+ ))} +
+ ) : ( +
No diff content available
)}
)} diff --git a/app/javascript/components/review/ReviewApp.jsx b/app/javascript/components/review/ReviewApp.jsx index 8fbbf22..a7352f3 100644 --- a/app/javascript/components/review/ReviewApp.jsx +++ b/app/javascript/components/review/ReviewApp.jsx @@ -121,17 +121,30 @@ const ReviewApp = () => { {error && ( -
+
{error}
)} + {status === 'failed' && !error && ( +
+ Review failed. This could be a GitHub API issue or an invalid PR URL. Try again. +
+ )} + + {!reviewId && status === 'idle' && !error && ( +
+ Paste a public GitHub PR URL above to start an AI-powered code review. +
+ )} + {reviewId && (
-

Files

+

Diff

diff --git a/app/javascript/components/review/ReviewPanel.jsx b/app/javascript/components/review/ReviewPanel.jsx index c280c30..58c3bd5 100644 --- a/app/javascript/components/review/ReviewPanel.jsx +++ b/app/javascript/components/review/ReviewPanel.jsx @@ -7,17 +7,30 @@ const ReviewPanel = ({ sections, synthesis, reviewId, status, onSynthesize, onFi const canSynthesize = status === 'complete' && hasSections && !synthesis?.verdict return ( -
+
{!hasSections && status === 'reviewing' && (
- Reviewing pull request... + Triaging files and starting review... +
+ )} + + {hasSections && status === 'reviewing' && ( +
+ + Reviewing more files... +
+ )} + + {status === 'complete' && !hasSections && ( +
+ No files were flagged for review.
)} {sections.map((section) => ( Date: Wed, 11 Mar 2026 09:37:22 +0000 Subject: [PATCH 2/2] Switch LLM provider from Anthropic to OpenAI gpt-4o-mini gpt-4o-mini is ~5-7x cheaper per review ($0.01-0.02 vs $0.07-0.10). Adds OpenAiClient with json_object response format, provider-based client selection in Pipeline and SynthesizeJob, and updates config defaults and initializer to use OPENAI_API_KEY. https://claude.ai/code/session_01W4XaE7NTEUQQTeRSNjKnE3 --- app/controllers/reviews_controller.rb | 2 +- app/jobs/synthesize_job.rb | 3 +- app/lib/harness/configuration.rb | 4 +- app/lib/harness/llm/open_ai_client.rb | 57 +++++++++++++++++++++++++++ app/lib/harness/review/pipeline.rb | 3 +- config/initializers/harness.rb | 5 ++- 6 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 app/lib/harness/llm/open_ai_client.rb diff --git a/app/controllers/reviews_controller.rb b/app/controllers/reviews_controller.rb index f71fa22..1b4486b 100644 --- a/app/controllers/reviews_controller.rb +++ b/app/controllers/reviews_controller.rb @@ -4,7 +4,7 @@ def index def create unless Harness.configuration&.api_key.present? - return render json: { error: "ANTHROPIC_API_KEY is not configured on the server." }, + return render json: { error: "OPENAI_API_KEY is not configured on the server." }, status: :service_unavailable end diff --git a/app/jobs/synthesize_job.rb b/app/jobs/synthesize_job.rb index 65202d1..7117646 100644 --- a/app/jobs/synthesize_job.rb +++ b/app/jobs/synthesize_job.rb @@ -7,7 +7,8 @@ def perform(review_id) human_comments = collect_comments(review) config = Harness.configuration - llm = Harness::LLM::AnthropicClient.new( + client_class = config.provider == :openai ? Harness::LLM::OpenAiClient : Harness::LLM::AnthropicClient + llm = client_class.new( api_key: config.api_key, model: config.model, max_tokens: config.max_tokens_per_call diff --git a/app/lib/harness/configuration.rb b/app/lib/harness/configuration.rb index adcede4..dca458c 100644 --- a/app/lib/harness/configuration.rb +++ b/app/lib/harness/configuration.rb @@ -4,8 +4,8 @@ class Configuration :on_section_complete def initialize - @provider = :anthropic - @model = "claude-sonnet-4-20250514" + @provider = :openai + @model = "gpt-4o-mini" @max_tokens_per_call = 4096 @on_section_complete = nil end diff --git a/app/lib/harness/llm/open_ai_client.rb b/app/lib/harness/llm/open_ai_client.rb new file mode 100644 index 0000000..9cc0055 --- /dev/null +++ b/app/lib/harness/llm/open_ai_client.rb @@ -0,0 +1,57 @@ +module Harness + module LLM + class OpenAiClient < Client + API_URL = "https://api.openai.com/v1/chat/completions" + + def initialize(api_key:, model:, max_tokens: 4096) + @api_key = api_key + @model = model + @max_tokens = max_tokens + end + + def complete(messages:, system: nil) + body = build_body(messages, system) + raw = post(body) + parse_response(raw) + end + + private + + def build_body(messages, system) + msgs = [] + msgs << { role: "system", content: system } if system + msgs.concat(messages.map(&:to_h)) + + { + model: @model, + max_tokens: @max_tokens, + messages: msgs, + response_format: { type: "json_object" } + }.to_json + end + + def post(body) + uri = URI(API_URL) + request = Net::HTTP::Post.new(uri) + request["Content-Type"] = "application/json" + request["Authorization"] = "Bearer #{@api_key}" + request.body = body + + response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| + http.request(request) + end + + JSON.parse(response.body, symbolize_names: true) + end + + def parse_response(raw) + Response.new( + content: raw.dig(:choices, 0, :message, :content) || "", + model: raw[:model] || @model, + input_tokens: raw.dig(:usage, :prompt_tokens) || 0, + output_tokens: raw.dig(:usage, :completion_tokens) || 0 + ) + end + end + end +end diff --git a/app/lib/harness/review/pipeline.rb b/app/lib/harness/review/pipeline.rb index 85df5c8..7aa9840 100644 --- a/app/lib/harness/review/pipeline.rb +++ b/app/lib/harness/review/pipeline.rb @@ -20,7 +20,8 @@ def call(pr_url:, pr_description: "") private def build_client - LLM::AnthropicClient.new( + client_class = @config.provider == :openai ? LLM::OpenAiClient : LLM::AnthropicClient + client_class.new( api_key: @config.api_key, model: @config.model, max_tokens: @config.max_tokens_per_call diff --git a/config/initializers/harness.rb b/config/initializers/harness.rb index 10bb94a..f212b6f 100644 --- a/config/initializers/harness.rb +++ b/config/initializers/harness.rb @@ -1,6 +1,7 @@ Rails.application.config.after_initialize do Harness.configure do |config| - config.api_key = ENV["ANTHROPIC_API_KEY"] - config.model = "claude-sonnet-4-20250514" + config.provider = :openai + config.api_key = ENV["OPENAI_API_KEY"] + config.model = "gpt-4o-mini" end end