Skip to content
Merged
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
9 changes: 7 additions & 2 deletions app/controllers/reviews_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ def index
end

def create
unless Harness.configuration&.api_key.present?
return render json: { error: "OPENAI_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 }
Expand Down Expand Up @@ -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
}
Expand Down
49 changes: 39 additions & 10 deletions app/javascript/components/review/DiffPanel.jsx
Original file line number Diff line number Diff line change
@@ -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({})

Expand All @@ -15,30 +22,52 @@ const DiffPanel = ({ sections, highlightFile, highlightRange }) => {
}

if (!sections || sections.length === 0) {
return <div className="text-sm text-gray-400 p-4">No diff data available</div>
if (status === 'reviewing' || status === 'submitting') {
return (
<div className="flex items-center gap-2 text-sm text-gray-400 p-4">
<span className="animate-spin inline-block w-4 h-4 border-2 border-gray-300 border-t-transparent rounded-full" />
Fetching diff...
</div>
)
}
return (
<div className="text-sm text-gray-400 p-4 text-center">
Submit a PR URL to see the diff here.
</div>
)
}

return (
<div className="space-y-2 overflow-y-auto">
<div className="space-y-2 overflow-y-auto max-h-[80vh]">
{sections.map((section) => (
<div
key={section.filename}
ref={el => fileRefs.current[section.filename] = el}
className="border border-gray-200 rounded"
className="border border-gray-200 rounded overflow-hidden"
>
<button
onClick={() => toggleFile(section.filename)}
className="w-full flex items-center justify-between px-3 py-2 bg-gray-50 hover:bg-gray-100 text-left text-sm"
>
<span className="font-mono text-xs">{section.filename}</span>
<div className="flex items-center gap-2">
<span className="font-mono text-xs">{section.filename}</span>
{section.language && section.language !== 'text' && (
<span className="text-xs bg-gray-200 text-gray-600 px-1.5 py-0.5 rounded">{section.language}</span>
)}
</div>
<span className="text-gray-400 text-xs">{collapsed[section.filename] ? '▸' : '▾'}</span>
</button>

{!collapsed[section.filename] && section.findings && (
<div className="p-2 text-xs text-gray-500">
{section.findings.length} finding{section.findings.length !== 1 ? 's' : ''}
{section.findings.some(f => f.severity === 'red_flag') && (
<span className="ml-2 text-red-600 font-medium">contains red flags</span>
{!collapsed[section.filename] && (
<div className="border-t border-gray-200">
{section.patch_text ? (
<pre className="text-xs leading-5 overflow-x-auto p-0 m-0">
{section.patch_text.split('\n').map((line, i) => (
<div key={i} className={`px-3 ${lineStyle(line)}`}>{line || ' '}</div>
))}
</pre>
) : (
<div className="p-3 text-xs text-gray-400 italic">No diff content available</div>
)}
</div>
)}
Expand Down
17 changes: 15 additions & 2 deletions app/javascript/components/review/ReviewApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,30 @@ const ReviewApp = () => {
<PrInput onSubmit={handleSubmit} disabled={isReviewing} />

{error && (
<div className="bg-red-50 border border-red-200 text-red-700 rounded-lg px-4 py-2 mb-4 text-sm">
<div className="bg-red-50 border border-red-200 text-red-700 rounded-lg px-4 py-3 mb-4 text-sm">
{error}
</div>
)}

{status === 'failed' && !error && (
<div className="bg-red-50 border border-red-200 text-red-700 rounded-lg px-4 py-3 mb-4 text-sm">
Review failed. This could be a GitHub API issue or an invalid PR URL. Try again.
</div>
)}

{!reviewId && status === 'idle' && !error && (
<div className="text-center py-12 text-gray-400 text-sm">
Paste a public GitHub PR URL above to start an AI-powered code review.
</div>
)}

{reviewId && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h2 className="text-sm font-medium text-gray-500 mb-3 uppercase tracking-wide">Files</h2>
<h2 className="text-sm font-medium text-gray-500 mb-3 uppercase tracking-wide">Diff</h2>
<DiffPanel
sections={sections}
status={status}
highlightFile={highlightFile}
highlightRange={highlightRange}
/>
Expand Down
19 changes: 16 additions & 3 deletions app/javascript/components/review/ReviewPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,30 @@ const ReviewPanel = ({ sections, synthesis, reviewId, status, onSynthesize, onFi
const canSynthesize = status === 'complete' && hasSections && !synthesis?.verdict

return (
<div className="space-y-4 overflow-y-auto">
<div className="space-y-4 overflow-y-auto max-h-[80vh]">
{!hasSections && status === 'reviewing' && (
<div className="flex items-center gap-2 text-sm text-gray-500 p-4">
<span className="animate-spin inline-block w-4 h-4 border-2 border-purple-500 border-t-transparent rounded-full" />
Reviewing pull request...
Triaging files and starting review...
</div>
)}

{hasSections && status === 'reviewing' && (
<div className="flex items-center gap-2 text-xs text-gray-400 px-1">
<span className="animate-spin inline-block w-3 h-3 border-2 border-purple-400 border-t-transparent rounded-full" />
Reviewing more files...
</div>
)}

{status === 'complete' && !hasSections && (
<div className="text-sm text-gray-400 p-4 text-center">
No files were flagged for review.
</div>
)}

{sections.map((section) => (
<SectionCard
key={section.id}
key={section.id || section.filename}
section={section}
reviewId={reviewId}
onFindingClick={onFindingClick}
Expand Down
1 change: 1 addition & 0 deletions app/jobs/review_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def build_callback(review)
section = review.review_sections.create!(
filename: file.filename,
language: file.language,
patch_text: file.patch_text,
walkthrough: "",
findings: findings.map { |f| finding_to_hash(f) },
status: ReviewSection::COMPLETE
Expand Down
3 changes: 2 additions & 1 deletion app/jobs/synthesize_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/lib/harness/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions app/lib/harness/llm/open_ai_client.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion app/lib/harness/review/pipeline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions config/initializers/harness.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddPatchTextToReviewSections < ActiveRecord::Migration[7.1]
def change
add_column :review_sections, :patch_text, :text
end
end
Loading