iris-chatbot 5.0.3 → 5.0.4

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": "iris-chatbot",
3
- "version": "5.0.3",
3
+ "version": "5.0.4",
4
4
  "private": false,
5
5
  "description": "One-command installer for the Iris project template.",
6
6
  "bin": {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "iris",
3
- "version": "5.0.3",
3
+ "version": "5.0.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "iris",
9
- "version": "5.0.3",
9
+ "version": "5.0.4",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.72.1",
12
12
  "clsx": "^2.1.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iris",
3
- "version": "5.0.3",
3
+ "version": "5.0.4",
4
4
  "private": true,
5
5
  "description": "One-command installer for the Iris project template.",
6
6
  "engines": {
@@ -524,6 +524,12 @@ button:focus-visible {
524
524
  text-underline-offset: 2px;
525
525
  }
526
526
 
527
+ .message-content a.message-content-link-badge {
528
+ margin: 0 2px;
529
+ vertical-align: baseline;
530
+ text-decoration: none;
531
+ }
532
+
527
533
  .source-badge-row {
528
534
  margin-top: 10px;
529
535
  display: flex;
@@ -531,6 +537,10 @@ button:focus-visible {
531
537
  gap: 8px;
532
538
  }
533
539
 
540
+ .message-content a.source-badge {
541
+ text-decoration: none;
542
+ }
543
+
534
544
  .source-badge {
535
545
  display: inline-flex;
536
546
  align-items: center;
@@ -539,18 +549,19 @@ button:focus-visible {
539
549
  border-radius: 999px;
540
550
  border: 1px solid var(--border);
541
551
  background: var(--panel-2);
542
- color: var(--text-secondary);
552
+ color: rgba(255, 255, 255, 0.88);
543
553
  text-decoration: none;
544
554
  font-size: 12px;
545
555
  line-height: 1;
546
556
  max-width: 220px;
547
- transition: border-color 0.18s ease, color 0.18s ease, background 0.18s ease;
557
+ transition: border-color 0.18s ease, color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease;
548
558
  }
549
559
 
550
560
  .source-badge:hover {
551
561
  border-color: var(--border-strong);
552
- color: var(--text-primary);
562
+ color: #ffffff;
553
563
  background: var(--panel-3);
564
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.35);
554
565
  }
555
566
 
556
567
  .source-badge-index {
@@ -561,15 +572,46 @@ button:focus-visible {
561
572
  height: 16px;
562
573
  border-radius: 999px;
563
574
  border: 1px solid var(--border-strong);
564
- color: var(--text-muted);
575
+ color: rgba(255, 255, 255, 0.78);
565
576
  font-size: 10px;
566
577
  font-weight: 700;
578
+ text-decoration: none;
579
+ }
580
+
581
+ [data-theme="light"] .source-badge {
582
+ color: #2d2d2d;
583
+ }
584
+
585
+ [data-theme="light"] .source-badge:hover {
586
+ color: #111111;
587
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
588
+ }
589
+
590
+ [data-theme="light"] .source-badge-index {
591
+ color: #4a4a4a;
592
+ }
593
+
594
+ [data-theme="light"] .source-badge:hover .source-badge-index {
595
+ color: #333333;
567
596
  }
568
597
 
569
598
  .source-badge-title {
570
599
  overflow: hidden;
571
600
  text-overflow: ellipsis;
572
601
  white-space: nowrap;
602
+ color: rgba(255, 255, 255, 0.88);
603
+ }
604
+
605
+ .source-badge:hover .source-badge-title {
606
+ color: #ffffff;
607
+ }
608
+
609
+ [data-theme="light"] .source-badge-title {
610
+ color: #1a1a1a;
611
+ }
612
+
613
+ [data-theme="light"] .source-badge:hover .source-badge-title {
614
+ color: #111111;
573
615
  }
574
616
 
575
617
  .message-content hr {
@@ -14,7 +14,13 @@ import {
14
14
  X,
15
15
  } from "lucide-react";
16
16
  import { memo, useMemo, useState } from "react";
17
- import type { MessageNode, Thread, ToolApproval, ToolEvent } from "../lib/types";
17
+ import type {
18
+ ChatCitationSource,
19
+ MessageNode,
20
+ Thread,
21
+ ToolApproval,
22
+ ToolEvent,
23
+ } from "../lib/types";
18
24
  import { splitContentAndSources } from "../lib/utils";
19
25
 
20
26
  const MAX_VISIBLE_TOOL_ITEMS = 8;
@@ -45,6 +51,11 @@ function MarkdownTable({
45
51
  );
46
52
  }
47
53
 
54
+ /** Remove parentheses that wrap a single markdown link so the badge is not shown inside (). */
55
+ function stripParenthesesAroundLinks(content: string): string {
56
+ return content.replace(/\s*\(\s*(\[[^\]]*\]\([^)]+\))\s*\)/g, " $1 ");
57
+ }
58
+
48
59
  function normalizeMathDelimiters(content: string) {
49
60
  // Convert TeX delimiters to remark-math compatible delimiters.
50
61
  return content
@@ -640,6 +651,47 @@ function sourceBadgeLabel(url: string, title?: string): string {
640
651
  }
641
652
  }
642
653
 
654
+ function normUrl(u: string): string {
655
+ try {
656
+ const p = new URL(u);
657
+ return p.origin + p.pathname.replace(/\/$/, "");
658
+ } catch {
659
+ return u;
660
+ }
661
+ }
662
+
663
+ /** Extract markdown link URLs from content in order of first appearance (deduped by normalized URL). */
664
+ function extractCitationOrderFromContent(content: string): string[] {
665
+ const ordered: string[] = [];
666
+ const seen = new Set<string>();
667
+ const re = /\[([^\]]*)\]\(([^)]+)\)/g;
668
+ let m;
669
+ while ((m = re.exec(content)) !== null) {
670
+ const url = m[2].trim();
671
+ const n = normUrl(url);
672
+ if (!seen.has(n)) {
673
+ seen.add(n);
674
+ ordered.push(n);
675
+ }
676
+ }
677
+ return ordered;
678
+ }
679
+
680
+ /** Reorder sources so their order matches first appearance of each URL in the content. */
681
+ function reorderSourcesByCitationOrder(
682
+ content: string,
683
+ sources: ChatCitationSource[],
684
+ ): ChatCitationSource[] {
685
+ const order = extractCitationOrderFromContent(content);
686
+ if (order.length === 0) return sources;
687
+ const orderIndex = (url: string) => {
688
+ const n = normUrl(url);
689
+ const i = order.indexOf(n);
690
+ return i >= 0 ? i : 1e9;
691
+ };
692
+ return [...sources].sort((a, b) => orderIndex(a.url) - orderIndex(b.url));
693
+ }
694
+
643
695
  function getTimelineVisual(event: ToolEvent): {
644
696
  chipLabel: string;
645
697
  chipClassName: string;
@@ -726,9 +778,16 @@ function MessageCard({
726
778
  () => splitContentAndSources(message.content || ""),
727
779
  [message.content],
728
780
  );
781
+ const sourcesOrderedByCitation = useMemo(
782
+ () => reorderSourcesByCitationOrder(messageTextContent, messageSources),
783
+ [messageTextContent, messageSources],
784
+ );
729
785
  const isStreamingPlaceholder = isAssistant && isStreaming && !messageTextContent;
730
786
  const assistantContent = useMemo(
731
- () => normalizeMathDelimiters(normalizeMarkdownStructure(messageTextContent)),
787
+ () =>
788
+ stripParenthesesAroundLinks(
789
+ normalizeMathDelimiters(normalizeMarkdownStructure(messageTextContent)),
790
+ ),
732
791
  [messageTextContent],
733
792
  );
734
793
  const renderedAssistantContent = isStreaming
@@ -825,6 +884,30 @@ function MessageCard({
825
884
  rehypePlugins={[rehypeKatex]}
826
885
  components={{
827
886
  table: ({ children }) => <MarkdownTable>{children}</MarkdownTable>,
887
+ a: ({ href, children }) => {
888
+ if (!href) return <a>{children}</a>;
889
+ const idx = sourcesOrderedByCitation.findIndex((s) =>
890
+ normUrl(s.url) === normUrl(href)
891
+ );
892
+ const matchedSource = idx >= 0 ? sourcesOrderedByCitation[idx] : null;
893
+ const rawLabel = sourceBadgeLabel(href, matchedSource?.title);
894
+ const label = rawLabel.replace(/^[\s(]+|[\s)]+$/g, "").trim() || rawLabel;
895
+ const index = idx >= 0 ? idx + 1 : 0;
896
+ return (
897
+ <a
898
+ className="source-badge message-content-link-badge"
899
+ href={href}
900
+ target="_blank"
901
+ rel="noreferrer noopener"
902
+ title={href}
903
+ >
904
+ {index > 0 ? (
905
+ <span className="source-badge-index">{index}</span>
906
+ ) : null}
907
+ <span className="source-badge-title">{label}</span>
908
+ </a>
909
+ );
910
+ },
828
911
  }}
829
912
  >
830
913
  {renderedAssistantContent}
@@ -834,9 +917,9 @@ function MessageCard({
834
917
  <p>{messageTextContent}</p>
835
918
  )}
836
919
 
837
- {message.role === "assistant" && !assistantCollapsed && messageSources.length > 0 ? (
920
+ {message.role === "assistant" && !assistantCollapsed && sourcesOrderedByCitation.length > 0 ? (
838
921
  <div className="source-badge-row" aria-label="Sources">
839
- {messageSources.map((source, index) => (
922
+ {sourcesOrderedByCitation.map((source, index) => (
840
923
  <a
841
924
  key={`${source.url}-${index}`}
842
925
  className="source-badge"