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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyd-shared-badge",
3
- "version": "0.3.38",
3
+ "version": "0.3.40",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -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 ai_usage_summary = assessmentResult?.ai_usage_summary;
588
- const label = 'AI Transparency';
589
- const topMovers = ai_usage_summary?.key_findings || [];
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
- <GaugeCard
592
- key={'ai-card'}
593
- title={'KYD AI (Beta)'}
594
- description={'Indicates the degree to which AI-assisted code is explicitly disclosed across analyzed files.'}
595
- percent={ai_usage_summary?.transparency_score}
596
- label={label}
597
- topMovers={topMovers.map(t => ({ label: t, uid: 'ai-usage' }))}
598
- topMoversTitle={'Key Findings'}
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>
@@ -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) return;
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 openrouter = createOpenAI({
46
- apiKey: process.env.OPENROUTER_API_KEY,
47
- baseURL: 'https://openrouter.ai/api/v1',
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
- const result = await streamText({
51
- model: openrouter('openai/o4-mini'),
52
- system,
53
- messages: [...history, { role: 'user' as const, content }],
54
- maxTokens: 1024,
55
- temperature: 0.2,
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: number;
209
- transparency_score: number;
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
- transparency_score: number;
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