kyd-shared-badge 0.3.38 → 0.3.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/SharedBadgeDisplay.tsx +70 -12
- package/src/chat/ChatWidget.tsx +2 -3
- package/src/lib/routes.ts +83 -26
- package/src/types.ts +16 -4
package/package.json
CHANGED
|
@@ -583,20 +583,78 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
583
583
|
const AiSection = () => (
|
|
584
584
|
<div className={`${wrapperMaxWidth} mx-auto`}>
|
|
585
585
|
<div className={'rounded-xl shadow-xl p-6 sm:p-8 mt-6 border'} style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}>
|
|
586
|
+
<Reveal headless={isHeadless} as={'h4'} offsetY={8} className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>KYD AI (Beta)</Reveal>
|
|
586
587
|
{(() => {
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
const
|
|
588
|
+
const ai = assessmentResult?.ai_usage_summary;
|
|
589
|
+
if (!ai) return null;
|
|
590
|
+
const techGauge = {
|
|
591
|
+
percent: Math.max(0, Math.min(100, Number(ai.originality_score ?? 0))),
|
|
592
|
+
label: 'Originality',
|
|
593
|
+
};
|
|
594
|
+
const riskGauge = {
|
|
595
|
+
percent: Math.max(0, Math.min(100, Number(ai.transparency_score ?? 0))),
|
|
596
|
+
label: ai.transparency_descriptor ? `Transparency — ${ai.transparency_descriptor}` : 'Transparency',
|
|
597
|
+
};
|
|
598
|
+
const stats: Array<{ label: string; value: string }>= [
|
|
599
|
+
{ label: 'Files analyzed', value: String(ai.files_analyzed ?? 0) },
|
|
600
|
+
{ label: 'Files with AI findings', value: String(ai.files_with_ai_findings ?? 0) },
|
|
601
|
+
{ label: 'Files with disclosure', value: String(ai.files_with_disclosure ?? 0) },
|
|
602
|
+
{ label: 'Repos analyzed', value: String(ai.repos_analyzed ?? 0) },
|
|
603
|
+
{ label: 'Repos with AI findings', value: String(ai.repos_with_ai_findings ?? 0) },
|
|
604
|
+
];
|
|
605
|
+
const findings = Array.isArray(ai.evidence) ? ai.evidence.slice(0, 5) : [];
|
|
606
|
+
const topMovers = Array.isArray(ai.key_findings) ? ai.key_findings : [];
|
|
590
607
|
return (
|
|
591
|
-
<
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
608
|
+
<div className="space-y-8">
|
|
609
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 *:min-h-full">
|
|
610
|
+
<GaugeCard
|
|
611
|
+
key={'ai-originality'}
|
|
612
|
+
title={'Originality'}
|
|
613
|
+
description={'Estimated share of human-authored code based on linguistic signals in comments.'}
|
|
614
|
+
percent={techGauge.percent}
|
|
615
|
+
label={techGauge.label}
|
|
616
|
+
topMovers={[]}
|
|
617
|
+
/>
|
|
618
|
+
<GaugeCard
|
|
619
|
+
key={'ai-transparency'}
|
|
620
|
+
title={'Transparency'}
|
|
621
|
+
description={'Proportion of AI-influenced files that include an explicit disclosure.'}
|
|
622
|
+
percent={riskGauge.percent}
|
|
623
|
+
label={riskGauge.label}
|
|
624
|
+
topMovers={topMovers.map(t => ({ label: t, uid: 'ai-usage' }))}
|
|
625
|
+
topMoversTitle={'Key Findings'}
|
|
626
|
+
/>
|
|
627
|
+
</div>
|
|
628
|
+
<div className="grid grid-cols-2 sm:grid-cols-5 gap-4">
|
|
629
|
+
{stats.map((s, i) => (
|
|
630
|
+
<div key={i} className={'rounded-md p-3 border'} style={{ borderColor: 'var(--icon-button-secondary)', background: 'var(--content-card-background)' }}>
|
|
631
|
+
<div className={'text-xs'} style={{ color: 'var(--text-secondary)' }}>{s.label}</div>
|
|
632
|
+
<div className={'text-lg font-semibold'} style={{ color: 'var(--text-main)' }}>{s.value}</div>
|
|
633
|
+
</div>
|
|
634
|
+
))}
|
|
635
|
+
</div>
|
|
636
|
+
{findings.length > 0 && (
|
|
637
|
+
<div>
|
|
638
|
+
<div className={'text-sm font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Sample Findings</div>
|
|
639
|
+
<div className="space-y-3">
|
|
640
|
+
{findings.map((f, idx) => (
|
|
641
|
+
<div key={idx} className={'rounded-md p-3 border'} style={{ borderColor: 'var(--icon-button-secondary)', background: 'var(--content-card-background)' }}>
|
|
642
|
+
<div className={'text-xs mb-1'} style={{ color: 'var(--text-secondary)' }}>{f.file_path || 'file'}</div>
|
|
643
|
+
{f.snippet ? (
|
|
644
|
+
<pre className={'text-xs overflow-auto p-2 rounded'} style={{ background: 'rgba(0,0,0,0.04)', color: 'var(--text-main)' }}>{f.snippet}</pre>
|
|
645
|
+
) : null}
|
|
646
|
+
{f.reason ? (
|
|
647
|
+
<div className={'text-xs mt-2'} style={{ color: 'var(--text-secondary)' }}>{f.reason}</div>
|
|
648
|
+
) : null}
|
|
649
|
+
</div>
|
|
650
|
+
))}
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
)}
|
|
654
|
+
{ai.explanation && (
|
|
655
|
+
<div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>{ai.explanation}</div>
|
|
656
|
+
)}
|
|
657
|
+
</div>
|
|
600
658
|
);
|
|
601
659
|
})()}
|
|
602
660
|
</div>
|
package/src/chat/ChatWidget.tsx
CHANGED
|
@@ -54,8 +54,7 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
54
54
|
|
|
55
55
|
useEffect(() => {
|
|
56
56
|
// when sidebar is closed, dismiss hint
|
|
57
|
-
if (open)
|
|
58
|
-
setShowHint(false);
|
|
57
|
+
if (!open) setShowHint(false);
|
|
59
58
|
}, [open]);
|
|
60
59
|
|
|
61
60
|
// Sidebar width with bounds and persistence
|
|
@@ -278,7 +277,7 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
278
277
|
)}
|
|
279
278
|
</div>
|
|
280
279
|
) : (
|
|
281
|
-
<div style={{ position: 'fixed', right: 0, top: Math.max(headerTop + 16, tabTop), height: 160, width: 44 }}>
|
|
280
|
+
<div style={{ position: 'fixed', right: 0, top: Math.max(headerTop + 16, tabTop), height: 160, width: 44, overflow: 'hidden' }}>
|
|
282
281
|
<button
|
|
283
282
|
aria-label={'Open chat sidebar'}
|
|
284
283
|
aria-expanded={open}
|
package/src/lib/routes.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { createSession } from './chat-store';
|
|
|
20
20
|
|
|
21
21
|
export const runtime = 'nodejs';
|
|
22
22
|
|
|
23
|
+
|
|
23
24
|
export async function chatStreamRoute(req: NextRequest, userId: string, companyId?: string) {
|
|
24
25
|
try {
|
|
25
26
|
|
|
@@ -41,43 +42,99 @@ export async function chatStreamRoute(req: NextRequest, userId: string, companyI
|
|
|
41
42
|
const graphData = await getReportGraphData(badgeId);
|
|
42
43
|
const system = buildAllContextPrompt(cleaned, graphData, { concise: true });
|
|
43
44
|
const history = await getHistory(sessionId, 20);
|
|
45
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
46
|
+
if (!apiKey) {
|
|
47
|
+
return Response.json({ error: 'Server misconfigured: missing OPENROUTER_API_KEY' }, { status: 500 });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const chatMessages = [
|
|
51
|
+
{ role: 'system', content: system },
|
|
52
|
+
...history.map((m: any) => ({ role: m.role, content: m.content })),
|
|
53
|
+
{ role: 'user', content },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const upstream = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
model: 'openai/o4-mini',
|
|
64
|
+
messages: chatMessages,
|
|
65
|
+
stream: true,
|
|
66
|
+
max_tokens: 1024,
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!upstream.ok || !upstream.body) {
|
|
71
|
+
const errText = await upstream.text().catch(() => '');
|
|
72
|
+
return Response.json({ error: `Upstream error: ${upstream.status}`, details: errText }, { status: 502 });
|
|
73
|
+
}
|
|
44
74
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
75
|
+
const encoder = new TextEncoder();
|
|
76
|
+
let sseBuffer = '';
|
|
77
|
+
let assistantText = '';
|
|
78
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
79
|
+
start(controller) {
|
|
80
|
+
const reader = upstream.body!.getReader();
|
|
81
|
+
const decoder = new TextDecoder();
|
|
82
|
+
const pump = async (): Promise<void> => {
|
|
83
|
+
try {
|
|
84
|
+
const { value, done } = await reader.read();
|
|
85
|
+
if (done) {
|
|
86
|
+
try { if (assistantText) await putMessage({ sessionId, role: 'assistant', content: assistantText }); } catch {}
|
|
87
|
+
controller.close();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
sseBuffer += decoder.decode(value, { stream: true });
|
|
91
|
+
const lines = sseBuffer.split(/\r?\n/);
|
|
92
|
+
sseBuffer = lines.pop() ?? '';
|
|
93
|
+
for (const l of lines) {
|
|
94
|
+
const line = l.trim();
|
|
95
|
+
if (!line.startsWith('data:')) continue;
|
|
96
|
+
const data = line.slice(5).trim();
|
|
97
|
+
if (!data) continue;
|
|
98
|
+
if (data === '[DONE]') {
|
|
99
|
+
try { if (assistantText) await putMessage({ sessionId, role: 'assistant', content: assistantText }); } catch {}
|
|
100
|
+
controller.close();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const json = JSON.parse(data);
|
|
105
|
+
const delta = json?.choices?.[0]?.delta?.content ?? '';
|
|
106
|
+
if (delta) {
|
|
107
|
+
assistantText += delta;
|
|
108
|
+
controller.enqueue(encoder.encode(delta));
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
112
|
+
pump();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
controller.error(err);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
pump();
|
|
118
|
+
},
|
|
119
|
+
cancel() {
|
|
120
|
+
try { (upstream as any).body?.cancel?.(); } catch {}
|
|
121
|
+
},
|
|
48
122
|
});
|
|
49
123
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// onFinish: async ({ text }: { text: string }) => {
|
|
57
|
-
// // Attempt to capture evidence block if present (UI also parses, but cache server-side)
|
|
58
|
-
// const evidence = tryParseEvidenceServer(text);
|
|
59
|
-
// await putMessage({ sessionId, role: 'assistant', content: text, evidenceJson: evidence || undefined });
|
|
60
|
-
// },
|
|
124
|
+
return new Response(stream, {
|
|
125
|
+
headers: {
|
|
126
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
127
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
128
|
+
'X-Accel-Buffering': 'no',
|
|
129
|
+
},
|
|
61
130
|
});
|
|
62
131
|
|
|
63
|
-
return result.toTextStreamResponse();
|
|
64
132
|
} catch (e) {
|
|
65
133
|
console.error('chat error', e);
|
|
66
134
|
return new Response('An error occurred. Please try again.', { status: 500 });
|
|
67
135
|
}
|
|
68
136
|
}
|
|
69
137
|
|
|
70
|
-
// function tryParseEvidenceServer(text: string): any | null {
|
|
71
|
-
// try {
|
|
72
|
-
// const start = text.indexOf('```json');
|
|
73
|
-
// const end = text.indexOf('```', start + 7);
|
|
74
|
-
// const raw = start >= 0 && end > start ? text.slice(start + 7, end).trim() : text.trim();
|
|
75
|
-
// const obj = JSON.parse(raw);
|
|
76
|
-
// if (obj && obj.type === 'evidence' && Array.isArray(obj.claims)) return obj;
|
|
77
|
-
// } catch {}
|
|
78
|
-
// return null;
|
|
79
|
-
// }
|
|
80
|
-
|
|
81
138
|
export async function createSessionRoute(req: NextRequest, userId: string, companyId?: string) {
|
|
82
139
|
try {
|
|
83
140
|
const body = await req.json().catch(() => ({}));
|
package/src/types.ts
CHANGED
|
@@ -203,10 +203,16 @@ export interface AssessmentResult {
|
|
|
203
203
|
ai_usage_summary?: {
|
|
204
204
|
explanation: string;
|
|
205
205
|
key_findings: string[];
|
|
206
|
+
originality_score: number;
|
|
207
|
+
transparency_score: number;
|
|
208
|
+
transparency_descriptor?: string;
|
|
206
209
|
files_with_ai_findings: number;
|
|
207
210
|
files_analyzed: number;
|
|
208
|
-
files_with_disclosure
|
|
209
|
-
|
|
211
|
+
files_with_disclosure?: number;
|
|
212
|
+
repos_analyzed?: number;
|
|
213
|
+
repos_with_ai_findings?: number;
|
|
214
|
+
evidence?: Array<{ file_path?: string; snippet?: string; reason?: string }>;
|
|
215
|
+
findings_by_repo?: Record<string, Array<{ file_path?: string; snippet?: string; reason?: string }>>;
|
|
210
216
|
};
|
|
211
217
|
key_skills?: string[];
|
|
212
218
|
summary_scores?: {
|
|
@@ -394,13 +400,19 @@ export interface GraphInsightsPayload {
|
|
|
394
400
|
top_movers?: Array<{ label?: string; uid?: string }>;
|
|
395
401
|
};
|
|
396
402
|
};
|
|
397
|
-
ai_usage_summary
|
|
403
|
+
ai_usage_summary?: {
|
|
398
404
|
explanation: string;
|
|
399
405
|
key_findings: string[];
|
|
406
|
+
originality_score: number;
|
|
407
|
+
transparency_score: number;
|
|
408
|
+
transparency_descriptor?: string;
|
|
400
409
|
files_with_ai_findings: number;
|
|
401
410
|
files_analyzed: number;
|
|
402
411
|
files_with_disclosure?: number;
|
|
403
|
-
|
|
412
|
+
repos_analyzed?: number;
|
|
413
|
+
repos_with_ai_findings?: number;
|
|
414
|
+
evidence?: Array<{ file_path?: string; snippet?: string; reason?: string }>;
|
|
415
|
+
findings_by_repo?: Record<string, Array<{ file_path?: string; snippet?: string; reason?: string }>>;
|
|
404
416
|
}
|
|
405
417
|
}
|
|
406
418
|
|