bluera-knowledge 0.37.0 → 0.37.1

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/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ BASE_STYLES,
3
4
  ZilAdapter,
4
5
  runMCPServer,
5
6
  spawnBackgroundWorker
6
- } from "./chunk-AO45YFHO.js";
7
+ } from "./chunk-VB5V4RC7.js";
7
8
  import {
8
9
  IntelligentCrawler,
9
10
  getCrawlStrategy
@@ -1371,6 +1372,8 @@ Search: "${query}"`);
1371
1372
  }
1372
1373
 
1373
1374
  // src/cli/commands/serve.ts
1375
+ import { exec } from "child_process";
1376
+ import { platform } from "os";
1374
1377
  import { serve } from "@hono/node-server";
1375
1378
  import { Command as Command6 } from "commander";
1376
1379
 
@@ -1380,6 +1383,2070 @@ import { join as join2 } from "path";
1380
1383
  import { Hono } from "hono";
1381
1384
  import { cors } from "hono/cors";
1382
1385
  import { z } from "zod";
1386
+
1387
+ // src/server/ui/admin.ts
1388
+ var ADMIN_STYLES = `
1389
+ /* \u2500\u2500\u2500 Layout \u2500\u2500\u2500 */
1390
+ body { margin: 0; }
1391
+ #app { display: flex; flex-direction: column; height: 100vh; }
1392
+
1393
+ .app-shell { display: flex; flex: 1; min-height: 0; overflow: hidden; }
1394
+
1395
+ /* \u2500\u2500\u2500 Sidebar \u2500\u2500\u2500 */
1396
+ .sidebar {
1397
+ width: 220px;
1398
+ flex-shrink: 0;
1399
+ background: var(--bg-secondary);
1400
+ border-right: 1px solid var(--border-primary);
1401
+ display: flex;
1402
+ flex-direction: column;
1403
+ overflow-y: auto;
1404
+ }
1405
+
1406
+ .sidebar-brand {
1407
+ padding: var(--space-lg);
1408
+ border-bottom: 1px solid var(--border-secondary);
1409
+ display: flex;
1410
+ align-items: center;
1411
+ gap: var(--space-sm);
1412
+ }
1413
+
1414
+ .sidebar-brand-icon {
1415
+ width: 28px;
1416
+ height: 28px;
1417
+ border-radius: var(--radius-md);
1418
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
1419
+ display: flex;
1420
+ align-items: center;
1421
+ justify-content: center;
1422
+ flex-shrink: 0;
1423
+ }
1424
+
1425
+ .sidebar-brand-text {
1426
+ font-size: var(--font-size-md);
1427
+ font-weight: 700;
1428
+ color: var(--text-primary);
1429
+ white-space: nowrap;
1430
+ }
1431
+
1432
+ .sidebar-nav { padding: var(--space-sm); flex: 1; }
1433
+
1434
+ .nav-item {
1435
+ display: flex;
1436
+ align-items: center;
1437
+ gap: var(--space-sm);
1438
+ padding: var(--space-sm) var(--space-md);
1439
+ border-radius: var(--radius-md);
1440
+ color: var(--text-secondary);
1441
+ text-decoration: none;
1442
+ font-size: var(--font-size-md);
1443
+ cursor: pointer;
1444
+ transition: background var(--transition-fast), color var(--transition-fast);
1445
+ border: none;
1446
+ background: none;
1447
+ width: 100%;
1448
+ text-align: left;
1449
+ font-family: var(--font-sans);
1450
+ }
1451
+
1452
+ .nav-item:hover { background: var(--bg-tertiary); color: var(--text-primary); }
1453
+ .nav-item.active { background: rgba(88, 166, 255, 0.1); color: var(--accent-blue); }
1454
+ .nav-item svg { flex-shrink: 0; }
1455
+
1456
+ .sidebar-footer {
1457
+ padding: var(--space-md) var(--space-lg);
1458
+ border-top: 1px solid var(--border-secondary);
1459
+ font-size: var(--font-size-xs);
1460
+ color: var(--text-tertiary);
1461
+ }
1462
+
1463
+ /* \u2500\u2500\u2500 Main Content \u2500\u2500\u2500 */
1464
+ .main-content {
1465
+ flex: 1;
1466
+ overflow-y: auto;
1467
+ padding: var(--space-xl) var(--space-2xl);
1468
+ min-width: 0;
1469
+ }
1470
+
1471
+ .page-header {
1472
+ display: flex;
1473
+ align-items: center;
1474
+ justify-content: space-between;
1475
+ margin-bottom: var(--space-xl);
1476
+ flex-wrap: wrap;
1477
+ gap: var(--space-md);
1478
+ }
1479
+
1480
+ .page-title {
1481
+ font-size: var(--font-size-2xl);
1482
+ font-weight: 700;
1483
+ color: var(--text-primary);
1484
+ }
1485
+
1486
+ .page-subtitle {
1487
+ font-size: var(--font-size-sm);
1488
+ color: var(--text-secondary);
1489
+ margin-top: var(--space-xs);
1490
+ }
1491
+
1492
+ /* \u2500\u2500\u2500 Breadcrumb \u2500\u2500\u2500 */
1493
+ .breadcrumb {
1494
+ display: flex;
1495
+ align-items: center;
1496
+ gap: var(--space-xs);
1497
+ font-size: var(--font-size-sm);
1498
+ color: var(--text-tertiary);
1499
+ margin-bottom: var(--space-lg);
1500
+ }
1501
+
1502
+ .breadcrumb a {
1503
+ color: var(--text-link);
1504
+ text-decoration: none;
1505
+ cursor: pointer;
1506
+ }
1507
+
1508
+ .breadcrumb a:hover { text-decoration: underline; }
1509
+
1510
+ .breadcrumb-sep {
1511
+ color: var(--text-tertiary);
1512
+ margin: 0 var(--space-xs);
1513
+ }
1514
+
1515
+ .breadcrumb-current { color: var(--text-secondary); }
1516
+
1517
+ /* \u2500\u2500\u2500 Stats bar \u2500\u2500\u2500 */
1518
+ .stats-bar {
1519
+ display: flex;
1520
+ gap: var(--space-lg);
1521
+ margin-bottom: var(--space-xl);
1522
+ flex-wrap: wrap;
1523
+ }
1524
+
1525
+ .stat-card {
1526
+ background: var(--bg-secondary);
1527
+ border: 1px solid var(--border-primary);
1528
+ border-radius: var(--radius-lg);
1529
+ padding: var(--space-md) var(--space-lg);
1530
+ display: flex;
1531
+ flex-direction: column;
1532
+ min-width: 100px;
1533
+ }
1534
+
1535
+ .stat-value {
1536
+ font-size: var(--font-size-2xl);
1537
+ font-weight: 700;
1538
+ color: var(--text-primary);
1539
+ line-height: 1.2;
1540
+ }
1541
+
1542
+ .stat-label {
1543
+ font-size: var(--font-size-xs);
1544
+ color: var(--text-tertiary);
1545
+ text-transform: uppercase;
1546
+ letter-spacing: 0.05em;
1547
+ margin-top: 2px;
1548
+ }
1549
+
1550
+ /* \u2500\u2500\u2500 Store card grid \u2500\u2500\u2500 */
1551
+ .store-grid {
1552
+ display: grid;
1553
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
1554
+ gap: var(--space-lg);
1555
+ }
1556
+
1557
+ @media (max-width: 768px) {
1558
+ .store-grid { grid-template-columns: 1fr; }
1559
+ .sidebar { display: none; }
1560
+ .main-content { padding: var(--space-lg); }
1561
+ .stats-bar { flex-direction: column; }
1562
+ }
1563
+
1564
+ .store-card {
1565
+ background: var(--bg-card);
1566
+ border: 1px solid var(--border-primary);
1567
+ border-radius: var(--radius-lg);
1568
+ overflow: hidden;
1569
+ transition: border-color var(--transition-normal), box-shadow var(--transition-normal);
1570
+ display: flex;
1571
+ flex-direction: column;
1572
+ cursor: pointer;
1573
+ text-decoration: none;
1574
+ color: inherit;
1575
+ }
1576
+
1577
+ .store-card:hover {
1578
+ border-color: rgba(88, 166, 255, 0.3);
1579
+ box-shadow: var(--shadow-md);
1580
+ background: var(--bg-card-hover);
1581
+ }
1582
+
1583
+ .card-header {
1584
+ padding: var(--space-lg) var(--space-lg) var(--space-sm);
1585
+ }
1586
+
1587
+ .card-title-row {
1588
+ display: flex;
1589
+ align-items: center;
1590
+ gap: var(--space-sm);
1591
+ }
1592
+
1593
+ .card-status { display: flex; align-items: center; flex-shrink: 0; }
1594
+
1595
+ .card-title {
1596
+ font-size: var(--font-size-md);
1597
+ font-weight: 600;
1598
+ color: var(--text-primary);
1599
+ flex: 1;
1600
+ min-width: 0;
1601
+ overflow: hidden;
1602
+ text-overflow: ellipsis;
1603
+ white-space: nowrap;
1604
+ }
1605
+
1606
+ .card-description {
1607
+ font-size: var(--font-size-sm);
1608
+ color: var(--text-secondary);
1609
+ margin-top: var(--space-sm);
1610
+ line-height: 1.5;
1611
+ display: -webkit-box;
1612
+ -webkit-line-clamp: 2;
1613
+ -webkit-box-orient: vertical;
1614
+ overflow: hidden;
1615
+ }
1616
+
1617
+ .card-body {
1618
+ padding: var(--space-sm) var(--space-lg);
1619
+ flex: 1;
1620
+ }
1621
+
1622
+ .card-location {
1623
+ color: var(--text-tertiary);
1624
+ font-size: var(--font-size-sm);
1625
+ }
1626
+
1627
+ .card-tags {
1628
+ display: flex;
1629
+ flex-wrap: wrap;
1630
+ gap: var(--space-xs);
1631
+ margin-top: var(--space-sm);
1632
+ }
1633
+
1634
+ .card-footer {
1635
+ padding: var(--space-sm) var(--space-lg) var(--space-lg);
1636
+ border-top: 1px solid var(--border-secondary);
1637
+ margin-top: auto;
1638
+ }
1639
+
1640
+ /* \u2500\u2500\u2500 Buttons \u2500\u2500\u2500 */
1641
+ .btn {
1642
+ display: inline-flex;
1643
+ align-items: center;
1644
+ gap: var(--space-sm);
1645
+ padding: var(--space-sm) var(--space-lg);
1646
+ border-radius: var(--radius-md);
1647
+ font-size: var(--font-size-md);
1648
+ font-weight: 500;
1649
+ font-family: var(--font-sans);
1650
+ cursor: pointer;
1651
+ border: 1px solid transparent;
1652
+ transition: background var(--transition-fast), border-color var(--transition-fast), opacity var(--transition-fast);
1653
+ white-space: nowrap;
1654
+ text-decoration: none;
1655
+ }
1656
+
1657
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
1658
+
1659
+ .btn-primary {
1660
+ background: var(--accent-blue);
1661
+ color: var(--text-inverse);
1662
+ border-color: var(--accent-blue);
1663
+ }
1664
+ .btn-primary:hover:not(:disabled) { opacity: 0.9; }
1665
+
1666
+ .btn-secondary {
1667
+ background: var(--bg-tertiary);
1668
+ color: var(--text-primary);
1669
+ border-color: var(--border-primary);
1670
+ }
1671
+ .btn-secondary:hover:not(:disabled) { background: var(--bg-card-hover); }
1672
+
1673
+ .btn-danger {
1674
+ background: rgba(248, 81, 73, 0.15);
1675
+ color: var(--accent-red);
1676
+ border-color: rgba(248, 81, 73, 0.3);
1677
+ }
1678
+ .btn-danger:hover:not(:disabled) { background: rgba(248, 81, 73, 0.25); }
1679
+
1680
+ .btn-sm {
1681
+ padding: var(--space-xs) var(--space-md);
1682
+ font-size: var(--font-size-sm);
1683
+ }
1684
+
1685
+ .btn-group {
1686
+ display: flex;
1687
+ gap: var(--space-sm);
1688
+ flex-wrap: wrap;
1689
+ }
1690
+
1691
+ /* \u2500\u2500\u2500 Forms \u2500\u2500\u2500 */
1692
+ .form-group {
1693
+ margin-bottom: var(--space-lg);
1694
+ }
1695
+
1696
+ .form-label {
1697
+ display: block;
1698
+ font-size: var(--font-size-sm);
1699
+ font-weight: 500;
1700
+ color: var(--text-primary);
1701
+ margin-bottom: var(--space-xs);
1702
+ }
1703
+
1704
+ .form-hint {
1705
+ font-size: var(--font-size-xs);
1706
+ color: var(--text-tertiary);
1707
+ margin-top: var(--space-xs);
1708
+ }
1709
+
1710
+ .form-input, .form-select, .form-textarea {
1711
+ width: 100%;
1712
+ padding: var(--space-sm) var(--space-md);
1713
+ background: var(--bg-inset);
1714
+ border: 1px solid var(--border-primary);
1715
+ border-radius: var(--radius-md);
1716
+ color: var(--text-primary);
1717
+ font-size: var(--font-size-md);
1718
+ font-family: var(--font-sans);
1719
+ transition: border-color var(--transition-fast);
1720
+ outline: none;
1721
+ }
1722
+
1723
+ .form-input:focus, .form-select:focus, .form-textarea:focus {
1724
+ border-color: var(--accent-blue);
1725
+ box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.2);
1726
+ }
1727
+
1728
+ .form-select {
1729
+ appearance: none;
1730
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='%238b949e'%3E%3Cpath d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
1731
+ background-repeat: no-repeat;
1732
+ background-position: right 10px center;
1733
+ padding-right: 32px;
1734
+ cursor: pointer;
1735
+ }
1736
+
1737
+ .form-select option { background: var(--bg-secondary); color: var(--text-primary); }
1738
+
1739
+ .form-textarea { resize: vertical; min-height: 80px; }
1740
+
1741
+ .form-row {
1742
+ display: grid;
1743
+ grid-template-columns: 1fr 1fr;
1744
+ gap: var(--space-lg);
1745
+ }
1746
+
1747
+ @media (max-width: 640px) {
1748
+ .form-row { grid-template-columns: 1fr; }
1749
+ }
1750
+
1751
+ .form-card {
1752
+ background: var(--bg-secondary);
1753
+ border: 1px solid var(--border-primary);
1754
+ border-radius: var(--radius-lg);
1755
+ padding: var(--space-xl);
1756
+ max-width: 640px;
1757
+ }
1758
+
1759
+ .form-error {
1760
+ color: var(--accent-red);
1761
+ font-size: var(--font-size-sm);
1762
+ margin-top: var(--space-sm);
1763
+ display: flex;
1764
+ align-items: center;
1765
+ gap: var(--space-xs);
1766
+ }
1767
+
1768
+ /* \u2500\u2500\u2500 Store Detail \u2500\u2500\u2500 */
1769
+ .detail-grid {
1770
+ display: grid;
1771
+ grid-template-columns: 1fr 1fr;
1772
+ gap: var(--space-lg);
1773
+ margin-bottom: var(--space-xl);
1774
+ }
1775
+
1776
+ @media (max-width: 768px) {
1777
+ .detail-grid { grid-template-columns: 1fr; }
1778
+ }
1779
+
1780
+ .detail-card {
1781
+ background: var(--bg-secondary);
1782
+ border: 1px solid var(--border-primary);
1783
+ border-radius: var(--radius-lg);
1784
+ padding: var(--space-xl);
1785
+ }
1786
+
1787
+ .detail-card-full {
1788
+ grid-column: 1 / -1;
1789
+ }
1790
+
1791
+ .detail-row {
1792
+ display: flex;
1793
+ justify-content: space-between;
1794
+ align-items: flex-start;
1795
+ padding: var(--space-sm) 0;
1796
+ border-bottom: 1px solid var(--border-secondary);
1797
+ }
1798
+
1799
+ .detail-row:last-child { border-bottom: none; }
1800
+
1801
+ .detail-key {
1802
+ font-size: var(--font-size-sm);
1803
+ color: var(--text-tertiary);
1804
+ text-transform: uppercase;
1805
+ letter-spacing: 0.04em;
1806
+ flex-shrink: 0;
1807
+ min-width: 110px;
1808
+ }
1809
+
1810
+ .detail-value {
1811
+ font-size: var(--font-size-sm);
1812
+ color: var(--text-primary);
1813
+ text-align: right;
1814
+ word-break: break-all;
1815
+ max-width: 70%;
1816
+ }
1817
+
1818
+ /* \u2500\u2500\u2500 Search \u2500\u2500\u2500 */
1819
+ .search-bar {
1820
+ display: flex;
1821
+ gap: var(--space-sm);
1822
+ margin-bottom: var(--space-lg);
1823
+ }
1824
+
1825
+ .search-bar .form-input {
1826
+ flex: 1;
1827
+ }
1828
+
1829
+ .search-options {
1830
+ display: flex;
1831
+ gap: var(--space-lg);
1832
+ margin-bottom: var(--space-lg);
1833
+ flex-wrap: wrap;
1834
+ align-items: flex-end;
1835
+ }
1836
+
1837
+ .search-option { min-width: 160px; }
1838
+
1839
+ .search-meta-bar {
1840
+ display: flex;
1841
+ align-items: center;
1842
+ gap: var(--space-md);
1843
+ flex-wrap: wrap;
1844
+ font-size: var(--font-size-sm);
1845
+ color: var(--text-secondary);
1846
+ margin-bottom: var(--space-lg);
1847
+ padding: var(--space-md) var(--space-lg);
1848
+ background: var(--bg-secondary);
1849
+ border: 1px solid var(--border-primary);
1850
+ border-radius: var(--radius-lg);
1851
+ }
1852
+
1853
+ .confidence-badge {
1854
+ display: inline-flex;
1855
+ align-items: center;
1856
+ padding: 2px 8px;
1857
+ border-radius: 9999px;
1858
+ font-size: var(--font-size-xs);
1859
+ font-weight: 500;
1860
+ text-transform: capitalize;
1861
+ }
1862
+
1863
+ .confidence-high { background: var(--confidence-high-bg); color: var(--confidence-high-text); }
1864
+ .confidence-medium { background: var(--confidence-medium-bg); color: var(--confidence-medium-text); }
1865
+ .confidence-low { background: var(--confidence-low-bg); color: var(--confidence-low-text); }
1866
+
1867
+ /* \u2500\u2500\u2500 Results list \u2500\u2500\u2500 */
1868
+ .results-list {
1869
+ display: flex;
1870
+ flex-direction: column;
1871
+ gap: var(--space-md);
1872
+ }
1873
+
1874
+ .result-card {
1875
+ background: var(--bg-card);
1876
+ border: 1px solid var(--border-primary);
1877
+ border-radius: var(--radius-lg);
1878
+ overflow: hidden;
1879
+ transition: border-color var(--transition-normal);
1880
+ }
1881
+
1882
+ .result-card:hover { border-color: rgba(88, 166, 255, 0.3); }
1883
+
1884
+ .result-header {
1885
+ display: flex;
1886
+ gap: var(--space-md);
1887
+ padding: var(--space-lg);
1888
+ }
1889
+
1890
+ .result-rank {
1891
+ flex-shrink: 0;
1892
+ width: 28px;
1893
+ height: 28px;
1894
+ display: flex;
1895
+ align-items: center;
1896
+ justify-content: center;
1897
+ background: var(--bg-tertiary);
1898
+ border: 1px solid var(--border-secondary);
1899
+ border-radius: var(--radius-sm);
1900
+ font-size: var(--font-size-sm);
1901
+ font-weight: 600;
1902
+ color: var(--text-tertiary);
1903
+ font-family: var(--font-mono);
1904
+ }
1905
+
1906
+ .result-main { flex: 1; min-width: 0; }
1907
+
1908
+ .result-title-row {
1909
+ display: flex;
1910
+ align-items: center;
1911
+ gap: var(--space-sm);
1912
+ margin-bottom: var(--space-xs);
1913
+ }
1914
+
1915
+ .type-icon {
1916
+ display: inline-flex;
1917
+ align-items: center;
1918
+ justify-content: center;
1919
+ width: 22px;
1920
+ height: 22px;
1921
+ border-radius: var(--radius-sm);
1922
+ font-size: 11px;
1923
+ font-weight: 700;
1924
+ font-family: var(--font-mono);
1925
+ flex-shrink: 0;
1926
+ }
1927
+
1928
+ .type-fn { background: rgba(88, 166, 255, 0.15); color: var(--accent-blue); }
1929
+ .type-class { background: rgba(210, 153, 34, 0.15); color: var(--accent-orange); }
1930
+ .type-iface { background: rgba(57, 210, 192, 0.15); color: var(--accent-cyan); }
1931
+ .type-type { background: rgba(188, 140, 255, 0.15); color: var(--accent-purple); }
1932
+ .type-const { background: rgba(63, 185, 80, 0.15); color: var(--accent-green); }
1933
+ .type-doc { background: rgba(139, 148, 158, 0.15); color: var(--text-secondary); }
1934
+ .type-example { background: rgba(247, 120, 186, 0.15); color: var(--accent-pink); }
1935
+ .type-default { background: rgba(139, 148, 158, 0.1); color: var(--text-tertiary); }
1936
+
1937
+ .result-name {
1938
+ font-size: var(--font-size-md);
1939
+ font-weight: 600;
1940
+ color: var(--text-primary);
1941
+ font-family: var(--font-mono);
1942
+ flex: 1;
1943
+ min-width: 0;
1944
+ overflow: hidden;
1945
+ text-overflow: ellipsis;
1946
+ white-space: nowrap;
1947
+ }
1948
+
1949
+ .result-signature {
1950
+ color: var(--text-secondary);
1951
+ margin-bottom: var(--space-sm);
1952
+ font-size: var(--font-size-sm);
1953
+ font-family: var(--font-mono);
1954
+ line-height: 1.4;
1955
+ overflow-x: auto;
1956
+ white-space: pre-wrap;
1957
+ word-break: break-all;
1958
+ }
1959
+
1960
+ .result-purpose {
1961
+ color: var(--text-secondary);
1962
+ font-size: var(--font-size-sm);
1963
+ margin-bottom: var(--space-sm);
1964
+ line-height: 1.5;
1965
+ }
1966
+
1967
+ .result-meta { margin-bottom: var(--space-xs); }
1968
+
1969
+ .result-location {
1970
+ color: var(--text-tertiary);
1971
+ font-size: var(--font-size-sm);
1972
+ }
1973
+
1974
+ .line-number { color: var(--accent-blue); }
1975
+
1976
+ .result-store {
1977
+ font-size: var(--font-size-sm);
1978
+ color: var(--text-tertiary);
1979
+ }
1980
+
1981
+ .result-relevance {
1982
+ font-size: var(--font-size-sm);
1983
+ color: var(--text-secondary);
1984
+ font-style: italic;
1985
+ margin-top: var(--space-xs);
1986
+ padding-left: var(--space-md);
1987
+ border-left: 2px solid var(--border-secondary);
1988
+ }
1989
+
1990
+ .score-high { color: var(--score-high); }
1991
+ .score-medium { color: var(--score-medium); }
1992
+ .score-low { color: var(--score-low); }
1993
+
1994
+ .result-card details { margin: 0; border-top: 1px solid var(--border-secondary); }
1995
+ .result-card details summary { padding: var(--space-sm) var(--space-lg); font-weight: 500; }
1996
+ .result-card .details-content { padding: 0 var(--space-lg) var(--space-lg); }
1997
+
1998
+ .context-group, .full-group { margin-bottom: var(--space-md); }
1999
+ .context-group:last-child, .full-group:last-child { margin-bottom: 0; }
2000
+
2001
+ .usage-stats { display: flex; gap: var(--space-lg); }
2002
+ .usage-stat { display: flex; align-items: baseline; gap: var(--space-xs); }
2003
+ .usage-value { font-size: var(--font-size-lg); font-weight: 700; font-family: var(--font-mono); color: var(--text-primary); }
2004
+ .usage-label { font-size: var(--font-size-xs); color: var(--text-tertiary); }
2005
+
2006
+ .documentation-text {
2007
+ font-size: var(--font-size-sm);
2008
+ color: var(--text-secondary);
2009
+ line-height: 1.6;
2010
+ white-space: pre-wrap;
2011
+ }
2012
+
2013
+ .result-card .code-block { max-height: 400px; overflow-y: auto; font-size: var(--font-size-sm); }
2014
+
2015
+ .code-block::-webkit-scrollbar { width: 6px; height: 6px; }
2016
+ .code-block::-webkit-scrollbar-track { background: transparent; }
2017
+ .code-block::-webkit-scrollbar-thumb { background: var(--border-primary); border-radius: 3px; }
2018
+ .code-block::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); }
2019
+
2020
+ /* \u2500\u2500\u2500 Modal \u2500\u2500\u2500 */
2021
+ .modal-overlay {
2022
+ position: fixed;
2023
+ inset: 0;
2024
+ background: rgba(0, 0, 0, 0.6);
2025
+ display: flex;
2026
+ align-items: center;
2027
+ justify-content: center;
2028
+ z-index: 1000;
2029
+ backdrop-filter: blur(4px);
2030
+ }
2031
+
2032
+ .modal {
2033
+ background: var(--bg-secondary);
2034
+ border: 1px solid var(--border-primary);
2035
+ border-radius: var(--radius-xl);
2036
+ padding: var(--space-xl);
2037
+ max-width: 420px;
2038
+ width: 90%;
2039
+ box-shadow: var(--shadow-lg);
2040
+ }
2041
+
2042
+ .modal-title {
2043
+ font-size: var(--font-size-lg);
2044
+ font-weight: 600;
2045
+ color: var(--text-primary);
2046
+ margin-bottom: var(--space-md);
2047
+ }
2048
+
2049
+ .modal-body {
2050
+ font-size: var(--font-size-sm);
2051
+ color: var(--text-secondary);
2052
+ margin-bottom: var(--space-xl);
2053
+ line-height: 1.5;
2054
+ }
2055
+
2056
+ .modal-actions {
2057
+ display: flex;
2058
+ justify-content: flex-end;
2059
+ gap: var(--space-sm);
2060
+ }
2061
+
2062
+ /* \u2500\u2500\u2500 Toast \u2500\u2500\u2500 */
2063
+ .toast-container {
2064
+ position: fixed;
2065
+ top: var(--space-lg);
2066
+ right: var(--space-lg);
2067
+ z-index: 2000;
2068
+ display: flex;
2069
+ flex-direction: column;
2070
+ gap: var(--space-sm);
2071
+ pointer-events: none;
2072
+ }
2073
+
2074
+ .toast {
2075
+ padding: var(--space-md) var(--space-lg);
2076
+ border-radius: var(--radius-lg);
2077
+ font-size: var(--font-size-sm);
2078
+ box-shadow: var(--shadow-lg);
2079
+ pointer-events: auto;
2080
+ animation: toast-in 300ms ease;
2081
+ max-width: 360px;
2082
+ display: flex;
2083
+ align-items: center;
2084
+ gap: var(--space-sm);
2085
+ }
2086
+
2087
+ .toast-error {
2088
+ background: rgba(248, 81, 73, 0.15);
2089
+ border: 1px solid rgba(248, 81, 73, 0.3);
2090
+ color: var(--accent-red);
2091
+ }
2092
+
2093
+ .toast-success {
2094
+ background: rgba(63, 185, 80, 0.15);
2095
+ border: 1px solid rgba(63, 185, 80, 0.3);
2096
+ color: var(--accent-green);
2097
+ }
2098
+
2099
+ .toast-info {
2100
+ background: rgba(88, 166, 255, 0.15);
2101
+ border: 1px solid rgba(88, 166, 255, 0.3);
2102
+ color: var(--accent-blue);
2103
+ }
2104
+
2105
+ @keyframes toast-in {
2106
+ from { opacity: 0; transform: translateY(-12px); }
2107
+ to { opacity: 1; transform: translateY(0); }
2108
+ }
2109
+
2110
+ /* \u2500\u2500\u2500 Spinner \u2500\u2500\u2500 */
2111
+ .spinner {
2112
+ width: 20px;
2113
+ height: 20px;
2114
+ border: 2px solid var(--border-primary);
2115
+ border-top-color: var(--accent-blue);
2116
+ border-radius: 50%;
2117
+ animation: spin 0.6s linear infinite;
2118
+ display: inline-block;
2119
+ flex-shrink: 0;
2120
+ }
2121
+
2122
+ .spinner-lg { width: 32px; height: 32px; border-width: 3px; }
2123
+
2124
+ @keyframes spin {
2125
+ to { transform: rotate(360deg); }
2126
+ }
2127
+
2128
+ .loading-center {
2129
+ display: flex;
2130
+ align-items: center;
2131
+ justify-content: center;
2132
+ padding: var(--space-2xl);
2133
+ gap: var(--space-md);
2134
+ color: var(--text-secondary);
2135
+ font-size: var(--font-size-sm);
2136
+ }
2137
+
2138
+ /* \u2500\u2500\u2500 Skeleton \u2500\u2500\u2500 */
2139
+ .skeleton {
2140
+ background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--bg-card-hover) 50%, var(--bg-tertiary) 75%);
2141
+ background-size: 200% 100%;
2142
+ animation: skeleton-pulse 1.5s ease-in-out infinite;
2143
+ border-radius: var(--radius-md);
2144
+ }
2145
+
2146
+ @keyframes skeleton-pulse {
2147
+ 0% { background-position: 200% 0; }
2148
+ 100% { background-position: -200% 0; }
2149
+ }
2150
+
2151
+ .skeleton-card {
2152
+ height: 180px;
2153
+ border-radius: var(--radius-lg);
2154
+ }
2155
+
2156
+ .skeleton-line {
2157
+ height: 16px;
2158
+ margin-bottom: var(--space-sm);
2159
+ }
2160
+
2161
+ .skeleton-line-short { width: 60%; }
2162
+
2163
+ /* \u2500\u2500\u2500 Multi-select (store filter) \u2500\u2500\u2500 */
2164
+ .multi-select {
2165
+ position: relative;
2166
+ }
2167
+
2168
+ .multi-select-trigger {
2169
+ display: flex;
2170
+ align-items: center;
2171
+ justify-content: space-between;
2172
+ padding: var(--space-sm) var(--space-md);
2173
+ background: var(--bg-inset);
2174
+ border: 1px solid var(--border-primary);
2175
+ border-radius: var(--radius-md);
2176
+ color: var(--text-primary);
2177
+ font-size: var(--font-size-md);
2178
+ cursor: pointer;
2179
+ min-height: 38px;
2180
+ font-family: var(--font-sans);
2181
+ }
2182
+
2183
+ .multi-select-trigger:hover { border-color: var(--accent-blue); }
2184
+
2185
+ .multi-select-dropdown {
2186
+ position: absolute;
2187
+ top: 100%;
2188
+ left: 0;
2189
+ right: 0;
2190
+ background: var(--bg-secondary);
2191
+ border: 1px solid var(--border-primary);
2192
+ border-radius: var(--radius-md);
2193
+ box-shadow: var(--shadow-lg);
2194
+ z-index: 100;
2195
+ margin-top: var(--space-xs);
2196
+ max-height: 200px;
2197
+ overflow-y: auto;
2198
+ display: none;
2199
+ }
2200
+
2201
+ .multi-select-dropdown.open { display: block; }
2202
+
2203
+ .multi-select-option {
2204
+ display: flex;
2205
+ align-items: center;
2206
+ gap: var(--space-sm);
2207
+ padding: var(--space-sm) var(--space-md);
2208
+ cursor: pointer;
2209
+ font-size: var(--font-size-sm);
2210
+ color: var(--text-primary);
2211
+ transition: background var(--transition-fast);
2212
+ }
2213
+
2214
+ .multi-select-option:hover { background: var(--bg-tertiary); }
2215
+
2216
+ .multi-select-option input[type="checkbox"] {
2217
+ accent-color: var(--accent-blue);
2218
+ }
2219
+
2220
+ /* \u2500\u2500\u2500 Mobile nav toggle \u2500\u2500\u2500 */
2221
+ .mobile-nav-toggle {
2222
+ display: none;
2223
+ position: fixed;
2224
+ bottom: var(--space-lg);
2225
+ right: var(--space-lg);
2226
+ width: 48px;
2227
+ height: 48px;
2228
+ border-radius: 50%;
2229
+ background: var(--accent-blue);
2230
+ color: var(--text-inverse);
2231
+ border: none;
2232
+ cursor: pointer;
2233
+ z-index: 500;
2234
+ box-shadow: var(--shadow-lg);
2235
+ align-items: center;
2236
+ justify-content: center;
2237
+ }
2238
+
2239
+ @media (max-width: 768px) {
2240
+ .mobile-nav-toggle { display: flex; }
2241
+ .sidebar.mobile-open { display: flex; position: fixed; top: 0; left: 0; bottom: 0; z-index: 400; }
2242
+ }
2243
+
2244
+ /* \u2500\u2500\u2500 Misc \u2500\u2500\u2500 */
2245
+ .inline-search-section {
2246
+ margin-top: var(--space-xl);
2247
+ padding-top: var(--space-xl);
2248
+ border-top: 1px solid var(--border-primary);
2249
+ }
2250
+
2251
+ .section-title {
2252
+ font-size: var(--font-size-lg);
2253
+ font-weight: 600;
2254
+ color: var(--text-primary);
2255
+ margin-bottom: var(--space-lg);
2256
+ }
2257
+
2258
+ .store-actions {
2259
+ margin-bottom: var(--space-xl);
2260
+ }
2261
+
2262
+ /* \u2500\u2500\u2500 Documents section \u2500\u2500\u2500 */
2263
+ .documents-section {
2264
+ margin-top: var(--space-xl);
2265
+ padding-top: var(--space-xl);
2266
+ border-top: 1px solid var(--border-primary);
2267
+ }
2268
+
2269
+ .documents-header {
2270
+ display: flex;
2271
+ align-items: center;
2272
+ justify-content: space-between;
2273
+ margin-bottom: var(--space-lg);
2274
+ }
2275
+
2276
+ .documents-list {
2277
+ display: flex;
2278
+ flex-direction: column;
2279
+ gap: var(--space-md);
2280
+ }
2281
+
2282
+ .document-item {
2283
+ background: var(--bg-card);
2284
+ border: 1px solid var(--border-primary);
2285
+ border-radius: var(--radius-lg);
2286
+ overflow: hidden;
2287
+ transition: border-color var(--transition-normal);
2288
+ }
2289
+
2290
+ .document-item:hover {
2291
+ border-color: rgba(88, 166, 255, 0.3);
2292
+ }
2293
+
2294
+ .document-meta {
2295
+ display: flex;
2296
+ align-items: center;
2297
+ gap: var(--space-sm);
2298
+ padding: var(--space-md) var(--space-lg);
2299
+ background: var(--bg-tertiary);
2300
+ border-bottom: 1px solid var(--border-secondary);
2301
+ font-size: var(--font-size-sm);
2302
+ min-height: 40px;
2303
+ }
2304
+
2305
+ .document-index {
2306
+ display: inline-flex;
2307
+ align-items: center;
2308
+ justify-content: center;
2309
+ min-width: 24px;
2310
+ height: 24px;
2311
+ padding: 0 var(--space-xs);
2312
+ background: var(--bg-inset);
2313
+ border: 1px solid var(--border-secondary);
2314
+ border-radius: var(--radius-sm);
2315
+ font-size: var(--font-size-xs);
2316
+ font-family: var(--font-mono);
2317
+ color: var(--text-tertiary);
2318
+ flex-shrink: 0;
2319
+ }
2320
+
2321
+ .document-path {
2322
+ color: var(--text-link);
2323
+ overflow: hidden;
2324
+ text-overflow: ellipsis;
2325
+ white-space: nowrap;
2326
+ font-weight: 500;
2327
+ }
2328
+
2329
+ .document-chunk-label {
2330
+ color: var(--text-tertiary);
2331
+ font-size: var(--font-size-xs);
2332
+ white-space: nowrap;
2333
+ }
2334
+
2335
+ .document-content {
2336
+ margin: 0;
2337
+ padding: var(--space-lg) !important;
2338
+ min-height: 80px;
2339
+ max-height: 300px;
2340
+ overflow-y: auto;
2341
+ font-size: var(--font-size-sm) !important;
2342
+ line-height: 1.7 !important;
2343
+ white-space: pre-wrap !important;
2344
+ word-break: break-word;
2345
+ border: none !important;
2346
+ border-radius: 0 !important;
2347
+ background: var(--bg-inset) !important;
2348
+ }
2349
+
2350
+ .document-content::-webkit-scrollbar { width: 6px; }
2351
+ .document-content::-webkit-scrollbar-track { background: transparent; }
2352
+ .document-content::-webkit-scrollbar-thumb { background: var(--border-primary); border-radius: 3px; }
2353
+ .document-content::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); }
2354
+
2355
+ .documents-pagination {
2356
+ display: flex;
2357
+ align-items: center;
2358
+ justify-content: center;
2359
+ gap: var(--space-md);
2360
+ margin-top: var(--space-xl);
2361
+ padding: var(--space-lg) 0;
2362
+ border-top: 1px solid var(--border-secondary);
2363
+ }
2364
+
2365
+ .btn-sm {
2366
+ padding: var(--space-xs) var(--space-md);
2367
+ font-size: var(--font-size-sm);
2368
+ }
2369
+
2370
+ .text-secondary { color: var(--text-secondary); }
2371
+ .text-tertiary { color: var(--text-tertiary); }
2372
+ `;
2373
+ function renderAdminUI() {
2374
+ return `<!DOCTYPE html>
2375
+ <html lang="en">
2376
+ <head>
2377
+ <meta charset="UTF-8">
2378
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2379
+ <title>Bluera Knowledge - Admin</title>
2380
+ <style>${BASE_STYLES}${ADMIN_STYLES}</style>
2381
+ </head>
2382
+ <body>
2383
+ <div id="app"></div>
2384
+ <div class="toast-container" id="toast-container"></div>
2385
+ <div id="modal-root"></div>
2386
+ <script>
2387
+ (function() {
2388
+ 'use strict';
2389
+
2390
+ // \u2500\u2500\u2500 Utilities \u2500\u2500\u2500
2391
+
2392
+ function esc(str) {
2393
+ if (str == null) return '';
2394
+ return String(str)
2395
+ .replace(/&/g, '&amp;')
2396
+ .replace(/</g, '&lt;')
2397
+ .replace(/>/g, '&gt;')
2398
+ .replace(/"/g, '&quot;')
2399
+ .replace(/'/g, '&#039;');
2400
+ }
2401
+
2402
+ function formatDate(iso) {
2403
+ if (!iso) return '';
2404
+ var d = new Date(iso);
2405
+ var now = new Date();
2406
+ var diffMs = now.getTime() - d.getTime();
2407
+ var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
2408
+ if (diffDays === 0) return 'Today';
2409
+ if (diffDays === 1) return 'Yesterday';
2410
+ if (diffDays < 7) return diffDays + 'd ago';
2411
+ if (diffDays < 30) return Math.floor(diffDays / 7) + 'w ago';
2412
+ if (diffDays < 365) return Math.floor(diffDays / 30) + 'mo ago';
2413
+ return Math.floor(diffDays / 365) + 'y ago';
2414
+ }
2415
+
2416
+ function formatFullDate(iso) {
2417
+ if (!iso) return '';
2418
+ return new Date(iso).toLocaleString();
2419
+ }
2420
+
2421
+ // \u2500\u2500\u2500 SVG Icons \u2500\u2500\u2500
2422
+
2423
+ var ICONS = {
2424
+ stores: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1h-8a1 1 0 00-1 1v6.708A2.486 2.486 0 014.5 9h8V1.5zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/></svg>',
2425
+ search: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M10.68 11.74a6 6 0 01-7.922-8.982 6 6 0 018.982 7.922l3.04 3.04a.749.749 0 01-.326 1.275.749.749 0 01-.734-.215l-3.04-3.04zM11.5 7a4.499 4.499 0 10-8.997 0A4.499 4.499 0 0011.5 7z"/></svg>',
2426
+ plus: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 010 1.5H8.5v4.25a.75.75 0 01-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>',
2427
+ trash: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19a1.75 1.75 0 001.741-1.575l.66-6.6a.75.75 0 00-1.492-.15l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"/></svg>',
2428
+ refresh: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 2.5a5.487 5.487 0 00-4.131 1.869l1.204 1.204A.25.25 0 014.896 6H1.25A.25.25 0 011 5.75V2.104a.25.25 0 01.427-.177l1.38 1.38A7.001 7.001 0 0114.95 7.16a.75.75 0 11-1.49.178A5.501 5.501 0 008 2.5zM1.705 8.005a.75.75 0 01.834.656 5.501 5.501 0 009.592 2.97l-1.204-1.204a.25.25 0 01.177-.427h3.646a.25.25 0 01.25.25v3.646a.25.25 0 01-.427.177l-1.38-1.38A7.001 7.001 0 011.05 8.84a.75.75 0 01.656-.834z"/></svg>',
2429
+ repo: '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1h-8a1 1 0 00-1 1v6.708A2.486 2.486 0 014.5 9h8V1.5zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/></svg>',
2430
+ file: '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M3.75 1.5a.25.25 0 00-.25.25v11.5c0 .138.112.25.25.25h8.5a.25.25 0 00.25-.25V6H9.75A1.75 1.75 0 018 4.25V1.5H3.75zm5.75.56v2.19c0 .138.112.25.25.25h2.19L9.5 2.06zM2 1.75C2 .784 2.784 0 3.75 0h5.086c.464 0 .909.184 1.237.513l3.414 3.414c.329.328.513.773.513 1.237v8.086A1.75 1.75 0 0112.25 15h-8.5A1.75 1.75 0 012 13.25V1.75z"/></svg>',
2431
+ web: '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zM6.379 5.227A31.17 31.17 0 006 7.5h4c-.07-.77-.2-1.53-.379-2.273a4.7 4.7 0 01-.862.22 4.5 4.5 0 01-1.038 0 4.7 4.7 0 01-.862-.22zM5.43 7.5c.05-.9.155-1.79.42-2.667a8.2 8.2 0 01-.987-.652A6.54 6.54 0 003.538 7.5H5.43zm-1.893 1.5h1.893c.05.898.155 1.787.42 2.667-.326.217-.668.41-.987.652A6.54 6.54 0 013.538 9zm6.963 0c-.05.898-.155 1.787-.42 2.667.326.217.668.41.987.652A6.54 6.54 0 0012.462 9h-1.893zm1.893-1.5h-1.893c-.05-.9-.155-1.79-.42-2.667.326-.217.668-.41.987-.652A6.54 6.54 0 0112.462 7.5zM6 9h4c-.07.77-.2 1.53-.379 2.273a4.7 4.7 0 01-.862-.22 4.5 4.5 0 01-1.038 0 4.7 4.7 0 01-.862.22A31.17 31.17 0 016 9z"/></svg>',
2432
+ brand: '<svg width="16" height="16" viewBox="0 0 16 16" fill="white"><path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-2a1.5 1.5 0 113 0v4a1.5 1.5 0 01-3 0V6z"/></svg>',
2433
+ hamburger: '<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor"><path d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 010 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 010 1.5H1.75A.75.75 0 011 7.75zM1.75 12h12.5a.75.75 0 010 1.5H1.75a.75.75 0 010-1.5z"/></svg>',
2434
+ chevron: '<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor"><path d="M4.7 2.3a.75.75 0 000 1.06L7.94 6.64 4.7 9.88a.75.75 0 101.06 1.06l3.87-3.87a.75.75 0 000-1.06L5.76 2.14a.75.75 0 00-1.06.16z"/></svg>'
2435
+ };
2436
+
2437
+ // \u2500\u2500\u2500 API client \u2500\u2500\u2500
2438
+
2439
+ function api(method, path, body) {
2440
+ var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
2441
+ if (body !== undefined) opts.body = JSON.stringify(body);
2442
+ return fetch(path, opts).then(function(res) {
2443
+ return res.json().then(function(data) {
2444
+ if (!res.ok) throw new Error(data.error || 'Request failed (' + res.status + ')');
2445
+ return data;
2446
+ });
2447
+ });
2448
+ }
2449
+
2450
+ // \u2500\u2500\u2500 Toast notifications \u2500\u2500\u2500
2451
+
2452
+ function showToast(message, type) {
2453
+ var container = document.getElementById('toast-container');
2454
+ var toast = document.createElement('div');
2455
+ toast.className = 'toast toast-' + (type || 'info');
2456
+ toast.textContent = message;
2457
+ container.appendChild(toast);
2458
+ setTimeout(function() {
2459
+ toast.style.transition = 'opacity 300ms';
2460
+ toast.style.opacity = '0';
2461
+ setTimeout(function() { toast.remove(); }, 300);
2462
+ }, 4000);
2463
+ }
2464
+
2465
+ // \u2500\u2500\u2500 Modal \u2500\u2500\u2500
2466
+
2467
+ function showModal(title, body, onConfirm, confirmLabel, confirmClass) {
2468
+ var root = document.getElementById('modal-root');
2469
+ root.innerHTML =
2470
+ '<div class="modal-overlay" id="modal-overlay">' +
2471
+ '<div class="modal">' +
2472
+ '<div class="modal-title">' + esc(title) + '</div>' +
2473
+ '<div class="modal-body">' + esc(body) + '</div>' +
2474
+ '<div class="modal-actions">' +
2475
+ '<button class="btn btn-secondary" id="modal-cancel">Cancel</button>' +
2476
+ '<button class="btn ' + (confirmClass || 'btn-danger') + '" id="modal-confirm">' + esc(confirmLabel || 'Confirm') + '</button>' +
2477
+ '</div>' +
2478
+ '</div>' +
2479
+ '</div>';
2480
+ document.getElementById('modal-cancel').onclick = function() { root.innerHTML = ''; };
2481
+ document.getElementById('modal-overlay').onclick = function(e) {
2482
+ if (e.target === e.currentTarget) root.innerHTML = '';
2483
+ };
2484
+ document.getElementById('modal-confirm').onclick = function() {
2485
+ root.innerHTML = '';
2486
+ onConfirm();
2487
+ };
2488
+ }
2489
+
2490
+ function hideModal() {
2491
+ document.getElementById('modal-root').innerHTML = '';
2492
+ }
2493
+
2494
+ // \u2500\u2500\u2500 State \u2500\u2500\u2500
2495
+
2496
+ var state = {
2497
+ stores: [],
2498
+ storesLoaded: false,
2499
+ currentStore: null,
2500
+ searchResults: null,
2501
+ refreshTimer: null,
2502
+ mobileNavOpen: false
2503
+ };
2504
+
2505
+ // \u2500\u2500\u2500 Type helpers \u2500\u2500\u2500
2506
+
2507
+ function typeIcon(type) {
2508
+ switch (type) {
2509
+ case 'repo': return ICONS.repo;
2510
+ case 'file': return ICONS.file;
2511
+ case 'web': return ICONS.web;
2512
+ default: return '';
2513
+ }
2514
+ }
2515
+
2516
+ function storeStatus(store) {
2517
+ if (store.status === 'indexing') return 'indexing';
2518
+ if (store.status === 'error') return 'error';
2519
+ if (store.modelId) return 'ready';
2520
+ return 'unknown';
2521
+ }
2522
+
2523
+ function storeStatusLabel(store) {
2524
+ var s = storeStatus(store);
2525
+ switch (s) {
2526
+ case 'ready': return 'Ready';
2527
+ case 'indexing': return 'Indexing';
2528
+ case 'error': return 'Error';
2529
+ default: return 'Not indexed';
2530
+ }
2531
+ }
2532
+
2533
+ function scoreColor(score) {
2534
+ if (score >= 0.7) return 'var(--score-high)';
2535
+ if (score >= 0.4) return 'var(--score-medium)';
2536
+ return 'var(--score-low)';
2537
+ }
2538
+
2539
+ function scoreClass(score) {
2540
+ if (score >= 0.7) return 'high';
2541
+ if (score >= 0.4) return 'medium';
2542
+ return 'low';
2543
+ }
2544
+
2545
+ function resultTypeIcon(type) {
2546
+ var map = {
2547
+ 'function': '<span class="type-icon type-fn">fn</span>',
2548
+ 'class': '<span class="type-icon type-class">C</span>',
2549
+ 'interface': '<span class="type-icon type-iface">I</span>',
2550
+ 'type': '<span class="type-icon type-type">T</span>',
2551
+ 'const': '<span class="type-icon type-const">K</span>',
2552
+ 'documentation': '<span class="type-icon type-doc">D</span>',
2553
+ 'example': '<span class="type-icon type-example">E</span>'
2554
+ };
2555
+ return map[type] || '<span class="type-icon type-default">?</span>';
2556
+ }
2557
+
2558
+ // \u2500\u2500\u2500 Routing \u2500\u2500\u2500
2559
+
2560
+ function navigate(hash) {
2561
+ window.location.hash = hash;
2562
+ }
2563
+
2564
+ function getRoute() {
2565
+ var hash = window.location.hash || '#/';
2566
+ if (hash === '#/' || hash === '#/stores') return { view: 'dashboard' };
2567
+ var storeMatch = hash.match(/^#\\/stores\\/(.+)$/);
2568
+ if (storeMatch) return { view: 'detail', id: decodeURIComponent(storeMatch[1]) };
2569
+ if (hash === '#/create') return { view: 'create' };
2570
+ if (hash.startsWith('#/search')) {
2571
+ var params = new URLSearchParams(hash.replace('#/search', '').replace('?', ''));
2572
+ return { view: 'search', query: params.get('q') || '' };
2573
+ }
2574
+ return { view: 'dashboard' };
2575
+ }
2576
+
2577
+ // \u2500\u2500\u2500 Rendering \u2500\u2500\u2500
2578
+
2579
+ function renderApp() {
2580
+ clearInterval(state.refreshTimer);
2581
+ state.refreshTimer = null;
2582
+
2583
+ var route = getRoute();
2584
+ var navActive = route.view;
2585
+
2586
+ var html =
2587
+ '<div class="app-shell">' +
2588
+ renderSidebar(navActive) +
2589
+ '<div class="main-content" id="main-content">' +
2590
+ renderLoading() +
2591
+ '</div>' +
2592
+ '</div>' +
2593
+ '<button class="mobile-nav-toggle" id="mobile-nav-toggle">' + ICONS.hamburger + '</button>';
2594
+
2595
+ document.getElementById('app').innerHTML = html;
2596
+
2597
+ document.getElementById('mobile-nav-toggle').onclick = function() {
2598
+ var sidebar = document.querySelector('.sidebar');
2599
+ state.mobileNavOpen = !state.mobileNavOpen;
2600
+ if (state.mobileNavOpen) {
2601
+ sidebar.classList.add('mobile-open');
2602
+ } else {
2603
+ sidebar.classList.remove('mobile-open');
2604
+ }
2605
+ };
2606
+
2607
+ switch (route.view) {
2608
+ case 'dashboard': loadDashboard(); break;
2609
+ case 'detail': loadStoreDetail(route.id); break;
2610
+ case 'create': renderCreateStore(); break;
2611
+ case 'search': renderSearchView(route.query); break;
2612
+ default: loadDashboard();
2613
+ }
2614
+ }
2615
+
2616
+ function renderSidebar(active) {
2617
+ return (
2618
+ '<div class="sidebar">' +
2619
+ '<div class="sidebar-brand">' +
2620
+ '<div class="sidebar-brand-icon">' + ICONS.brand + '</div>' +
2621
+ '<span class="sidebar-brand-text">Knowledge</span>' +
2622
+ '</div>' +
2623
+ '<nav class="sidebar-nav">' +
2624
+ '<a class="nav-item' + (active === 'dashboard' ? ' active' : '') + '" href="#/stores">' +
2625
+ ICONS.stores + ' Stores' +
2626
+ '</a>' +
2627
+ '<a class="nav-item' + (active === 'search' ? ' active' : '') + '" href="#/search">' +
2628
+ ICONS.search + ' Search' +
2629
+ '</a>' +
2630
+ '<a class="nav-item' + (active === 'create' ? ' active' : '') + '" href="#/create">' +
2631
+ ICONS.plus + ' Create Store' +
2632
+ '</a>' +
2633
+ '</nav>' +
2634
+ '<div class="sidebar-footer">Bluera Knowledge Admin</div>' +
2635
+ '</div>'
2636
+ );
2637
+ }
2638
+
2639
+ function renderLoading() {
2640
+ return '<div class="loading-center"><div class="spinner spinner-lg"></div> Loading...</div>';
2641
+ }
2642
+
2643
+ function renderSkeletonCards(n) {
2644
+ var html = '<div class="store-grid">';
2645
+ for (var i = 0; i < n; i++) {
2646
+ html += '<div class="skeleton skeleton-card"></div>';
2647
+ }
2648
+ html += '</div>';
2649
+ return html;
2650
+ }
2651
+
2652
+ // \u2500\u2500\u2500 Dashboard \u2500\u2500\u2500
2653
+
2654
+ function loadDashboard() {
2655
+ var main = document.getElementById('main-content');
2656
+ main.innerHTML = renderSkeletonCards(6);
2657
+
2658
+ api('GET', '/api/stores').then(function(stores) {
2659
+ state.stores = stores;
2660
+ state.storesLoaded = true;
2661
+ renderDashboardContent();
2662
+
2663
+ state.refreshTimer = setInterval(function() {
2664
+ api('GET', '/api/stores').then(function(s) {
2665
+ state.stores = s;
2666
+ if (getRoute().view === 'dashboard') renderDashboardContent();
2667
+ }).catch(function() {});
2668
+ }, 10000);
2669
+ }).catch(function(err) {
2670
+ main.innerHTML = '<div class="empty-state"><div class="empty-state-title">Failed to load stores</div><p>' + esc(err.message) + '</p></div>';
2671
+ });
2672
+ }
2673
+
2674
+ function renderDashboardContent() {
2675
+ var main = document.getElementById('main-content');
2676
+ var stores = state.stores;
2677
+
2678
+ var repoCount = 0, fileCount = 0, webCount = 0, indexedCount = 0;
2679
+ for (var i = 0; i < stores.length; i++) {
2680
+ if (stores[i].type === 'repo') repoCount++;
2681
+ if (stores[i].type === 'file') fileCount++;
2682
+ if (stores[i].type === 'web') webCount++;
2683
+ if (stores[i].modelId) indexedCount++;
2684
+ }
2685
+
2686
+ var html =
2687
+ '<div class="page-header">' +
2688
+ '<div>' +
2689
+ '<h1 class="page-title">Knowledge Stores</h1>' +
2690
+ '<p class="page-subtitle">' + stores.length + ' store' + (stores.length !== 1 ? 's' : '') + ' configured</p>' +
2691
+ '</div>' +
2692
+ '<a class="btn btn-primary" href="#/create">' + ICONS.plus + ' Create Store</a>' +
2693
+ '</div>' +
2694
+ '<div class="stats-bar">' +
2695
+ renderStatCard(stores.length, 'Total') +
2696
+ renderStatCard(indexedCount, 'Indexed') +
2697
+ renderStatCard(stores.length - indexedCount, 'Pending') +
2698
+ (repoCount > 0 ? renderStatCard(repoCount, 'Repo') : '') +
2699
+ (fileCount > 0 ? renderStatCard(fileCount, 'File') : '') +
2700
+ (webCount > 0 ? renderStatCard(webCount, 'Web') : '') +
2701
+ '</div>';
2702
+
2703
+ if (stores.length === 0) {
2704
+ html += '<div class="empty-state"><div class="empty-state-title">No knowledge stores</div><p>Create a store to start indexing code and documentation.</p></div>';
2705
+ } else {
2706
+ html += '<div class="store-grid">';
2707
+ for (var j = 0; j < stores.length; j++) {
2708
+ html += renderStoreCard(stores[j]);
2709
+ }
2710
+ html += '</div>';
2711
+ }
2712
+
2713
+ main.innerHTML = html;
2714
+ bindCardClicks();
2715
+ }
2716
+
2717
+ function renderStatCard(value, label) {
2718
+ return (
2719
+ '<div class="stat-card">' +
2720
+ '<span class="stat-value">' + value + '</span>' +
2721
+ '<span class="stat-label">' + esc(label) + '</span>' +
2722
+ '</div>'
2723
+ );
2724
+ }
2725
+
2726
+ function renderStoreCard(store) {
2727
+ var st = storeStatus(store);
2728
+ var loc = store.path || store.url || '';
2729
+ var truncLoc = loc.length > 60 ? '...' + loc.slice(loc.length - 57) : loc;
2730
+
2731
+ var tagsHtml = '';
2732
+ if (store.tags && store.tags.length > 0) {
2733
+ tagsHtml = '<div class="card-tags">';
2734
+ for (var i = 0; i < store.tags.length; i++) {
2735
+ tagsHtml += '<span class="tag">' + esc(store.tags[i]) + '</span>';
2736
+ }
2737
+ tagsHtml += '</div>';
2738
+ }
2739
+
2740
+ var descHtml = store.description
2741
+ ? '<p class="card-description">' + esc(store.description) + '</p>'
2742
+ : '';
2743
+
2744
+ var branchHtml = store.branch
2745
+ ? '<span class="meta-separator"></span><span class="mono">' + esc(store.branch) + '</span>'
2746
+ : '';
2747
+
2748
+ return (
2749
+ '<div class="store-card" data-store-id="' + esc(store.id) + '">' +
2750
+ '<div class="card-header">' +
2751
+ '<div class="card-title-row">' +
2752
+ '<div class="card-status"><span class="status-dot status-' + st + '" title="' + esc(storeStatusLabel(store)) + '"></span></div>' +
2753
+ '<h3 class="card-title">' + esc(store.name) + '</h3>' +
2754
+ '<span class="badge badge-' + store.type + '">' + typeIcon(store.type) + '<span style="margin-left:4px">' + store.type + '</span></span>' +
2755
+ '</div>' +
2756
+ descHtml +
2757
+ '</div>' +
2758
+ '<div class="card-body">' +
2759
+ (loc ? '<div class="card-location mono truncate" title="' + esc(loc) + '">' + esc(truncLoc) + '</div>' : '') +
2760
+ tagsHtml +
2761
+ '</div>' +
2762
+ '<div class="card-footer"><div class="meta-row">' +
2763
+ '<span title="' + esc(store.createdAt) + '">' + formatDate(store.createdAt) + '</span>' +
2764
+ branchHtml +
2765
+ '</div></div>' +
2766
+ '</div>'
2767
+ );
2768
+ }
2769
+
2770
+ function bindCardClicks() {
2771
+ var cards = document.querySelectorAll('.store-card[data-store-id]');
2772
+ for (var i = 0; i < cards.length; i++) {
2773
+ (function(card) {
2774
+ card.onclick = function() {
2775
+ navigate('#/stores/' + encodeURIComponent(card.getAttribute('data-store-id')));
2776
+ };
2777
+ })(cards[i]);
2778
+ }
2779
+ }
2780
+
2781
+ // \u2500\u2500\u2500 Store Detail \u2500\u2500\u2500
2782
+
2783
+ function loadStoreDetail(id) {
2784
+ var main = document.getElementById('main-content');
2785
+ main.innerHTML = renderLoading();
2786
+
2787
+ api('GET', '/api/stores/' + encodeURIComponent(id)).then(function(store) {
2788
+ state.currentStore = store;
2789
+ renderStoreDetailContent(store);
2790
+ }).catch(function(err) {
2791
+ main.innerHTML =
2792
+ '<div class="breadcrumb"><a href="#/stores">Stores</a><span class="breadcrumb-sep">' + ICONS.chevron + '</span><span class="breadcrumb-current">Not found</span></div>' +
2793
+ '<div class="empty-state"><div class="empty-state-title">Store not found</div><p>' + esc(err.message) + '</p></div>';
2794
+ });
2795
+ }
2796
+
2797
+ function renderStoreDetailContent(store) {
2798
+ var main = document.getElementById('main-content');
2799
+ var st = storeStatus(store);
2800
+
2801
+ var rows = [];
2802
+
2803
+ // URL first for repo/web stores \u2014 externally linked
2804
+ if (store.url) rows.push({ key: 'URL', value: store.url, link: true });
2805
+
2806
+ rows.push({ key: 'ID', value: store.id });
2807
+ rows.push({ key: 'Name', value: store.name });
2808
+ rows.push({ key: 'Type', value: store.type });
2809
+ rows.push({ key: 'Status', value: storeStatusLabel(store) });
2810
+
2811
+ if (store.path) rows.push({ key: 'Path', value: store.path });
2812
+ if (store.branch) rows.push({ key: 'Branch', value: store.branch });
2813
+ if (store.depth != null) rows.push({ key: 'Depth', value: String(store.depth) });
2814
+ if (store.maxPages != null) rows.push({ key: 'Max Pages', value: String(store.maxPages) });
2815
+ if (store.modelId) rows.push({ key: 'Model', value: store.modelId });
2816
+ if (store.schemaVersion != null) rows.push({ key: 'Schema', value: 'v' + store.schemaVersion });
2817
+ rows.push({ key: 'Created', value: formatFullDate(store.createdAt) });
2818
+ rows.push({ key: 'Updated', value: formatFullDate(store.updatedAt) });
2819
+
2820
+ var detailRows = '';
2821
+ for (var i = 0; i < rows.length; i++) {
2822
+ var val = rows[i].link
2823
+ ? '<a href="' + esc(rows[i].value) + '" target="_blank" rel="noopener noreferrer" style="color:var(--text-link);text-decoration:none">' + esc(rows[i].value) + ' <span style="font-size:11px;opacity:0.6">\u2197</span></a>'
2824
+ : esc(rows[i].value);
2825
+ detailRows += '<div class="detail-row"><span class="detail-key">' + esc(rows[i].key) + '</span><span class="detail-value mono">' + val + '</span></div>';
2826
+ }
2827
+
2828
+ var tagsHtml = '';
2829
+ if (store.tags && store.tags.length > 0) {
2830
+ tagsHtml = '<div class="detail-row"><span class="detail-key">Tags</span><span class="detail-value"><div class="card-tags">';
2831
+ for (var t = 0; t < store.tags.length; t++) {
2832
+ tagsHtml += '<span class="tag">' + esc(store.tags[t]) + '</span>';
2833
+ }
2834
+ tagsHtml += '</div></span></div>';
2835
+ }
2836
+
2837
+ var descHtml = '';
2838
+ if (store.description) {
2839
+ descHtml = '<div class="detail-row"><span class="detail-key">Description</span><span class="detail-value" style="text-align:left;max-width:none;word-break:normal">' + esc(store.description) + '</span></div>';
2840
+ }
2841
+
2842
+ var html =
2843
+ '<div class="breadcrumb">' +
2844
+ '<a href="#/stores">Stores</a>' +
2845
+ '<span class="breadcrumb-sep">' + ICONS.chevron + '</span>' +
2846
+ '<span class="breadcrumb-current">' + esc(store.name) + '</span>' +
2847
+ '</div>' +
2848
+ '<div class="page-header">' +
2849
+ '<div style="display:flex;align-items:center;gap:var(--space-md)">' +
2850
+ '<span class="status-dot status-' + st + '" style="width:12px;height:12px" title="' + esc(storeStatusLabel(store)) + '"></span>' +
2851
+ '<h1 class="page-title">' + esc(store.name) + '</h1>' +
2852
+ '<span class="badge badge-' + store.type + '">' + typeIcon(store.type) + '<span style="margin-left:4px">' + store.type + '</span></span>' +
2853
+ '</div>' +
2854
+ '</div>' +
2855
+ '<div class="store-actions"><div class="btn-group">' +
2856
+ '<button class="btn btn-secondary" id="btn-reindex">' + ICONS.refresh + ' Re-index</button>' +
2857
+ '<button class="btn btn-danger" id="btn-delete">' + ICONS.trash + ' Delete</button>' +
2858
+ '</div></div>' +
2859
+ '<div class="detail-grid">' +
2860
+ '<div class="detail-card">' + detailRows + '</div>' +
2861
+ '<div class="detail-card">' + descHtml + tagsHtml +
2862
+ ((!store.description && (!store.tags || store.tags.length === 0))
2863
+ ? '<div class="empty-state" style="padding:var(--space-lg) 0"><p>No description or tags</p></div>'
2864
+ : '') +
2865
+ '</div>' +
2866
+ '</div>' +
2867
+ '<div class="inline-search-section">' +
2868
+ '<h2 class="section-title">Search in ' + esc(store.name) + '</h2>' +
2869
+ '<div class="search-bar">' +
2870
+ '<input class="form-input" id="detail-search-input" type="text" placeholder="Search within this store..." />' +
2871
+ '<button class="btn btn-primary" id="detail-search-btn">' + ICONS.search + ' Search</button>' +
2872
+ '</div>' +
2873
+ '<div id="detail-search-results"></div>' +
2874
+ '</div>' +
2875
+ '<div class="documents-section">' +
2876
+ '<h2 class="section-title">Indexed Chunks</h2>' +
2877
+ '<div id="documents-container"><div class="loading-center"><div class="spinner"></div> Loading chunks...</div></div>' +
2878
+ '</div>';
2879
+
2880
+ main.innerHTML = html;
2881
+
2882
+ // Load documents
2883
+ loadDocuments(store.id, 0);
2884
+
2885
+ // Bind actions
2886
+ document.getElementById('btn-reindex').onclick = function() {
2887
+ var btn = this;
2888
+ btn.disabled = true;
2889
+ btn.innerHTML = '<div class="spinner"></div> Indexing...';
2890
+ api('POST', '/api/stores/' + encodeURIComponent(store.id) + '/index').then(function() {
2891
+ showToast('Re-indexing complete', 'success');
2892
+ loadStoreDetail(store.id);
2893
+ }).catch(function(err) {
2894
+ showToast('Re-index failed: ' + err.message, 'error');
2895
+ btn.disabled = false;
2896
+ btn.innerHTML = ICONS.refresh + ' Re-index';
2897
+ });
2898
+ };
2899
+
2900
+ document.getElementById('btn-delete').onclick = function() {
2901
+ showModal(
2902
+ 'Delete Store',
2903
+ 'Are you sure you want to delete "' + store.name + '"? This action cannot be undone.',
2904
+ function() {
2905
+ api('DELETE', '/api/stores/' + encodeURIComponent(store.id)).then(function() {
2906
+ showToast('Store deleted', 'success');
2907
+ navigate('#/stores');
2908
+ }).catch(function(err) {
2909
+ showToast('Delete failed: ' + err.message, 'error');
2910
+ });
2911
+ },
2912
+ 'Delete',
2913
+ 'btn-danger'
2914
+ );
2915
+ };
2916
+
2917
+ var searchInput = document.getElementById('detail-search-input');
2918
+ var searchBtn = document.getElementById('detail-search-btn');
2919
+
2920
+ function doDetailSearch() {
2921
+ var q = searchInput.value.trim();
2922
+ if (!q) return;
2923
+ var resultsDiv = document.getElementById('detail-search-results');
2924
+ resultsDiv.innerHTML = '<div class="loading-center"><div class="spinner"></div> Searching...</div>';
2925
+ api('POST', '/api/search', { query: q, stores: [store.id], detail: 'contextual' }).then(function(data) {
2926
+ resultsDiv.innerHTML = renderSearchResults(data);
2927
+ }).catch(function(err) {
2928
+ resultsDiv.innerHTML = '<div class="form-error">' + esc(err.message) + '</div>';
2929
+ });
2930
+ }
2931
+
2932
+ searchBtn.onclick = doDetailSearch;
2933
+ searchInput.onkeydown = function(e) {
2934
+ if (e.key === 'Enter') doDetailSearch();
2935
+ };
2936
+ }
2937
+
2938
+ function loadDocuments(storeId, offset) {
2939
+ var limit = 15;
2940
+ var container = document.getElementById('documents-container');
2941
+ if (!container) return;
2942
+
2943
+ api('GET', '/api/stores/' + encodeURIComponent(storeId) + '/documents?limit=' + limit + '&offset=' + offset).then(function(data) {
2944
+ if (data.total === 0) {
2945
+ container.innerHTML = '<div class="empty-state" style="padding:var(--space-xl) 0"><p>No indexed chunks yet. Try re-indexing this store.</p></div>';
2946
+ return;
2947
+ }
2948
+
2949
+ var html = '<div class="documents-header">' +
2950
+ '<span class="text-secondary" style="font-size:var(--font-size-sm)">' + data.total + ' chunks indexed</span>' +
2951
+ '<span class="text-tertiary" style="font-size:var(--font-size-xs)">Page ' + (Math.floor(offset / limit) + 1) + ' of ' + Math.ceil(data.total / limit) + '</span>' +
2952
+ '</div>';
2953
+ html += '<div class="documents-list">';
2954
+ for (var i = 0; i < data.documents.length; i++) {
2955
+ var doc = data.documents[i];
2956
+ var path = (doc.metadata && doc.metadata.path) ? doc.metadata.path : 'unknown';
2957
+ var chunkLabel = '';
2958
+ if (doc.metadata && doc.metadata.chunkIndex != null) {
2959
+ chunkLabel = '<span class="document-chunk-label">chunk ' + doc.metadata.chunkIndex + ' of ' + (doc.metadata.totalChunks || '?') + '</span>';
2960
+ }
2961
+ var typeBadge = doc.metadata && doc.metadata.type
2962
+ ? '<span class="badge badge-' + (doc.metadata.type === 'web' ? 'web' : doc.metadata.type === 'chunk' ? 'file' : 'file') + '" style="font-size:10px;padding:1px 6px;margin-left:auto">' + esc(doc.metadata.type) + '</span>'
2963
+ : '';
2964
+ var preview = doc.content.length > 800 ? doc.content.substring(0, 800) + ' ... (truncated)' : doc.content;
2965
+
2966
+ html += '<div class="document-item">' +
2967
+ '<div class="document-meta">' +
2968
+ '<span class="document-index">' + (offset + i + 1) + '</span>' +
2969
+ '<span class="document-path mono">' + esc(path) + '</span>' +
2970
+ chunkLabel +
2971
+ typeBadge +
2972
+ '</div>' +
2973
+ '<pre class="document-content code-block">' + esc(preview) + '</pre>' +
2974
+ '</div>';
2975
+ }
2976
+ html += '</div>';
2977
+
2978
+ // Pagination
2979
+ html += '<div class="documents-pagination">';
2980
+ if (offset > 0) {
2981
+ html += '<button class="btn btn-secondary btn-sm" id="docs-prev">&larr; Previous</button>';
2982
+ }
2983
+ html += '<span class="text-secondary" style="font-size:var(--font-size-sm)">' +
2984
+ (offset + 1) + '\u2013' + Math.min(offset + limit, data.total) + ' of ' + data.total +
2985
+ '</span>';
2986
+ if (offset + limit < data.total) {
2987
+ html += '<button class="btn btn-secondary btn-sm" id="docs-next">Next &rarr;</button>';
2988
+ }
2989
+ html += '</div>';
2990
+
2991
+ container.innerHTML = html;
2992
+
2993
+ // Bind pagination
2994
+ var prevBtn = document.getElementById('docs-prev');
2995
+ if (prevBtn) prevBtn.onclick = function() { loadDocuments(storeId, Math.max(0, offset - limit)); };
2996
+ var nextBtn = document.getElementById('docs-next');
2997
+ if (nextBtn) nextBtn.onclick = function() { loadDocuments(storeId, offset + limit); };
2998
+ }).catch(function(err) {
2999
+ container.innerHTML = '<div class="form-error">' + esc(err.message) + '</div>';
3000
+ });
3001
+ }
3002
+
3003
+ // \u2500\u2500\u2500 Create Store \u2500\u2500\u2500
3004
+
3005
+ function renderCreateStore() {
3006
+ var main = document.getElementById('main-content');
3007
+
3008
+ var html =
3009
+ '<div class="breadcrumb">' +
3010
+ '<a href="#/stores">Stores</a>' +
3011
+ '<span class="breadcrumb-sep">' + ICONS.chevron + '</span>' +
3012
+ '<span class="breadcrumb-current">Create Store</span>' +
3013
+ '</div>' +
3014
+ '<h1 class="page-title" style="margin-bottom:var(--space-xl)">Create Store</h1>' +
3015
+ '<div class="form-card">' +
3016
+ '<div class="form-group">' +
3017
+ '<label class="form-label">Type</label>' +
3018
+ '<select class="form-select" id="create-type">' +
3019
+ '<option value="repo">Repository</option>' +
3020
+ '<option value="file">File / Directory</option>' +
3021
+ '<option value="web">Web</option>' +
3022
+ '</select>' +
3023
+ '</div>' +
3024
+ '<div class="form-group">' +
3025
+ '<label class="form-label">Name</label>' +
3026
+ '<input class="form-input" id="create-name" type="text" placeholder="my-project" />' +
3027
+ '</div>' +
3028
+ '<div class="form-group">' +
3029
+ '<label class="form-label">Description <span style="color:var(--text-tertiary)">(optional)</span></label>' +
3030
+ '<textarea class="form-textarea" id="create-description" placeholder="What this store contains..."></textarea>' +
3031
+ '</div>' +
3032
+ '<div id="create-dynamic-fields"></div>' +
3033
+ '<div class="form-group">' +
3034
+ '<label class="form-label">Tags <span style="color:var(--text-tertiary)">(optional, comma-separated)</span></label>' +
3035
+ '<input class="form-input" id="create-tags" type="text" placeholder="typescript, api, docs" />' +
3036
+ '</div>' +
3037
+ '<div id="create-error"></div>' +
3038
+ '<div class="btn-group" style="margin-top:var(--space-xl)">' +
3039
+ '<button class="btn btn-primary" id="create-submit">' + ICONS.plus + ' Create Store</button>' +
3040
+ '<a class="btn btn-secondary" href="#/stores">Cancel</a>' +
3041
+ '</div>' +
3042
+ '</div>';
3043
+
3044
+ main.innerHTML = html;
3045
+
3046
+ var typeSelect = document.getElementById('create-type');
3047
+ renderDynamicFields(typeSelect.value);
3048
+ typeSelect.onchange = function() { renderDynamicFields(this.value); };
3049
+
3050
+ document.getElementById('create-submit').onclick = submitCreateStore;
3051
+ }
3052
+
3053
+ function renderDynamicFields(type) {
3054
+ var container = document.getElementById('create-dynamic-fields');
3055
+ var html = '';
3056
+
3057
+ switch (type) {
3058
+ case 'repo':
3059
+ html =
3060
+ '<div class="form-row">' +
3061
+ '<div class="form-group">' +
3062
+ '<label class="form-label">URL <span style="color:var(--text-tertiary)">(or path)</span></label>' +
3063
+ '<input class="form-input" id="create-url" type="text" placeholder="https://github.com/org/repo" />' +
3064
+ '</div>' +
3065
+ '<div class="form-group">' +
3066
+ '<label class="form-label">Path <span style="color:var(--text-tertiary)">(or URL)</span></label>' +
3067
+ '<input class="form-input" id="create-path" type="text" placeholder="/path/to/repo" />' +
3068
+ '</div>' +
3069
+ '</div>' +
3070
+ '<div class="form-group">' +
3071
+ '<label class="form-label">Branch <span style="color:var(--text-tertiary)">(optional)</span></label>' +
3072
+ '<input class="form-input" id="create-branch" type="text" placeholder="main" />' +
3073
+ '</div>';
3074
+ break;
3075
+ case 'file':
3076
+ html =
3077
+ '<div class="form-group">' +
3078
+ '<label class="form-label">Path</label>' +
3079
+ '<input class="form-input" id="create-path" type="text" placeholder="/path/to/directory" />' +
3080
+ '<p class="form-hint">Absolute path to the file or directory to index</p>' +
3081
+ '</div>';
3082
+ break;
3083
+ case 'web':
3084
+ html =
3085
+ '<div class="form-group">' +
3086
+ '<label class="form-label">URL</label>' +
3087
+ '<input class="form-input" id="create-url" type="text" placeholder="https://docs.example.com" />' +
3088
+ '</div>' +
3089
+ '<div class="form-row">' +
3090
+ '<div class="form-group">' +
3091
+ '<label class="form-label">Depth <span style="color:var(--text-tertiary)">(optional)</span></label>' +
3092
+ '<input class="form-input" id="create-depth" type="number" min="1" placeholder="3" />' +
3093
+ '<p class="form-hint">How many levels deep to crawl</p>' +
3094
+ '</div>' +
3095
+ '<div class="form-group">' +
3096
+ '<label class="form-label">Max Pages <span style="color:var(--text-tertiary)">(optional)</span></label>' +
3097
+ '<input class="form-input" id="create-maxpages" type="number" min="1" placeholder="50" />' +
3098
+ '</div>' +
3099
+ '</div>';
3100
+ break;
3101
+ }
3102
+
3103
+ container.innerHTML = html;
3104
+ }
3105
+
3106
+ function submitCreateStore() {
3107
+ var btn = document.getElementById('create-submit');
3108
+ var errorDiv = document.getElementById('create-error');
3109
+ errorDiv.innerHTML = '';
3110
+
3111
+ var type = document.getElementById('create-type').value;
3112
+ var name = document.getElementById('create-name').value.trim();
3113
+
3114
+ if (!name) {
3115
+ errorDiv.innerHTML = '<div class="form-error">Name is required</div>';
3116
+ return;
3117
+ }
3118
+
3119
+ var body = { name: name, type: type };
3120
+
3121
+ var descEl = document.getElementById('create-description');
3122
+ if (descEl && descEl.value.trim()) body.description = descEl.value.trim();
3123
+
3124
+ var tagsEl = document.getElementById('create-tags');
3125
+ if (tagsEl && tagsEl.value.trim()) {
3126
+ body.tags = tagsEl.value.split(',').map(function(t) { return t.trim(); }).filter(function(t) { return t.length > 0; });
3127
+ }
3128
+
3129
+ var pathEl = document.getElementById('create-path');
3130
+ if (pathEl && pathEl.value.trim()) body.path = pathEl.value.trim();
3131
+
3132
+ var urlEl = document.getElementById('create-url');
3133
+ if (urlEl && urlEl.value.trim()) body.url = urlEl.value.trim();
3134
+
3135
+ var branchEl = document.getElementById('create-branch');
3136
+ if (branchEl && branchEl.value.trim()) body.branch = branchEl.value.trim();
3137
+
3138
+ var depthEl = document.getElementById('create-depth');
3139
+ if (depthEl && depthEl.value) body.depth = parseInt(depthEl.value, 10);
3140
+
3141
+ btn.disabled = true;
3142
+ btn.innerHTML = '<div class="spinner"></div> Creating...';
3143
+
3144
+ api('POST', '/api/stores', body).then(function(store) {
3145
+ showToast('Store "' + store.name + '" created', 'success');
3146
+ navigate('#/stores/' + encodeURIComponent(store.id));
3147
+ }).catch(function(err) {
3148
+ errorDiv.innerHTML = '<div class="form-error">' + esc(err.message) + '</div>';
3149
+ btn.disabled = false;
3150
+ btn.innerHTML = ICONS.plus + ' Create Store';
3151
+ });
3152
+ }
3153
+
3154
+ // \u2500\u2500\u2500 Search View \u2500\u2500\u2500
3155
+
3156
+ function renderSearchView(initialQuery) {
3157
+ var main = document.getElementById('main-content');
3158
+
3159
+ // First load stores for the filter
3160
+ api('GET', '/api/stores').then(function(stores) {
3161
+ state.stores = stores;
3162
+ renderSearchViewContent(stores, initialQuery);
3163
+ }).catch(function() {
3164
+ renderSearchViewContent([], initialQuery);
3165
+ });
3166
+ }
3167
+
3168
+ function renderSearchViewContent(stores, initialQuery) {
3169
+ var main = document.getElementById('main-content');
3170
+ var selectedStores = [];
3171
+
3172
+ var storeOptions = '';
3173
+ for (var i = 0; i < stores.length; i++) {
3174
+ storeOptions +=
3175
+ '<label class="multi-select-option">' +
3176
+ '<input type="checkbox" value="' + esc(stores[i].id) + '" />' +
3177
+ '<span class="badge badge-' + stores[i].type + '" style="font-size:10px;padding:1px 6px">' + stores[i].type + '</span>' +
3178
+ esc(stores[i].name) +
3179
+ '</label>';
3180
+ }
3181
+
3182
+ var html =
3183
+ '<h1 class="page-title" style="margin-bottom:var(--space-xl)">Search</h1>' +
3184
+ '<div class="search-bar">' +
3185
+ '<input class="form-input" id="search-input" type="text" placeholder="Search across all knowledge stores..." value="' + esc(initialQuery || '') + '" />' +
3186
+ '<button class="btn btn-primary" id="search-btn">' + ICONS.search + ' Search</button>' +
3187
+ '</div>' +
3188
+ '<div class="search-options">' +
3189
+ '<div class="search-option">' +
3190
+ '<label class="form-label">Detail Level</label>' +
3191
+ '<select class="form-select" id="search-detail">' +
3192
+ '<option value="minimal">Minimal</option>' +
3193
+ '<option value="contextual" selected>Contextual</option>' +
3194
+ '<option value="full">Full</option>' +
3195
+ '</select>' +
3196
+ '</div>' +
3197
+ '<div class="search-option">' +
3198
+ '<label class="form-label">Stores</label>' +
3199
+ '<div class="multi-select" id="store-filter">' +
3200
+ '<div class="multi-select-trigger" id="store-filter-trigger">All stores</div>' +
3201
+ '<div class="multi-select-dropdown" id="store-filter-dropdown">' +
3202
+ storeOptions +
3203
+ '</div>' +
3204
+ '</div>' +
3205
+ '</div>' +
3206
+ '</div>' +
3207
+ '<div id="search-results"></div>';
3208
+
3209
+ main.innerHTML = html;
3210
+
3211
+ // Multi-select behavior
3212
+ var trigger = document.getElementById('store-filter-trigger');
3213
+ var dropdown = document.getElementById('store-filter-dropdown');
3214
+
3215
+ trigger.onclick = function(e) {
3216
+ e.stopPropagation();
3217
+ dropdown.classList.toggle('open');
3218
+ };
3219
+
3220
+ document.addEventListener('click', function closeDropdown(e) {
3221
+ if (!dropdown.contains(e.target) && e.target !== trigger) {
3222
+ dropdown.classList.remove('open');
3223
+ }
3224
+ });
3225
+
3226
+ var checkboxes = dropdown.querySelectorAll('input[type="checkbox"]');
3227
+ for (var c = 0; c < checkboxes.length; c++) {
3228
+ checkboxes[c].onchange = function() {
3229
+ selectedStores = [];
3230
+ for (var k = 0; k < checkboxes.length; k++) {
3231
+ if (checkboxes[k].checked) selectedStores.push(checkboxes[k].value);
3232
+ }
3233
+ trigger.textContent = selectedStores.length === 0 ? 'All stores' : selectedStores.length + ' store' + (selectedStores.length > 1 ? 's' : '') + ' selected';
3234
+ };
3235
+ }
3236
+
3237
+ function doSearch() {
3238
+ var q = document.getElementById('search-input').value.trim();
3239
+ if (!q) return;
3240
+
3241
+ var detail = document.getElementById('search-detail').value;
3242
+ var resultsDiv = document.getElementById('search-results');
3243
+ resultsDiv.innerHTML = '<div class="loading-center"><div class="spinner"></div> Searching...</div>';
3244
+
3245
+ // Update URL
3246
+ var newHash = '#/search?q=' + encodeURIComponent(q);
3247
+ if (window.location.hash !== newHash) {
3248
+ history.replaceState(null, '', newHash);
3249
+ }
3250
+
3251
+ var searchBody = { query: q, detail: detail };
3252
+ if (selectedStores.length > 0) searchBody.stores = selectedStores;
3253
+
3254
+ api('POST', '/api/search', searchBody).then(function(data) {
3255
+ resultsDiv.innerHTML = renderSearchResults(data);
3256
+ }).catch(function(err) {
3257
+ resultsDiv.innerHTML = '<div class="form-error">' + esc(err.message) + '</div>';
3258
+ });
3259
+ }
3260
+
3261
+ document.getElementById('search-btn').onclick = doSearch;
3262
+ document.getElementById('search-input').onkeydown = function(e) {
3263
+ if (e.key === 'Enter') doSearch();
3264
+ };
3265
+
3266
+ // Auto-search if we have a query
3267
+ if (initialQuery) {
3268
+ doSearch();
3269
+ }
3270
+
3271
+ // Focus input
3272
+ document.getElementById('search-input').focus();
3273
+ }
3274
+
3275
+ // \u2500\u2500\u2500 Search Results Renderer \u2500\u2500\u2500
3276
+
3277
+ function renderSearchResults(data) {
3278
+ var results = data.results || [];
3279
+ var html = '';
3280
+
3281
+ // Meta bar
3282
+ var metaParts = [];
3283
+ metaParts.push('<strong>' + results.length + '</strong> result' + (results.length !== 1 ? 's' : ''));
3284
+ if (data.timeMs != null) metaParts.push(data.timeMs + 'ms');
3285
+ if (data.mode) metaParts.push(esc(data.mode) + ' mode');
3286
+
3287
+ var confidenceHtml = '';
3288
+ if (data.confidence) {
3289
+ var confClass = data.confidence === 'high' ? 'confidence-high' : data.confidence === 'medium' ? 'confidence-medium' : 'confidence-low';
3290
+ confidenceHtml = '<span class="meta-separator"></span><span class="confidence-badge ' + confClass + '">' + esc(data.confidence) + ' confidence</span>';
3291
+ }
3292
+
3293
+ html += '<div class="search-meta-bar">';
3294
+ html += metaParts.join('<span class="meta-separator"></span>');
3295
+ html += confidenceHtml;
3296
+ html += '</div>';
3297
+
3298
+ if (results.length === 0) {
3299
+ html += '<div class="empty-state"><div class="empty-state-title">No results found</div><p>Try broadening your search query or checking different stores.</p></div>';
3300
+ return html;
3301
+ }
3302
+
3303
+ html += '<div class="results-list">';
3304
+ for (var i = 0; i < results.length; i++) {
3305
+ html += renderResultCard(results[i], i);
3306
+ }
3307
+ html += '</div>';
3308
+
3309
+ return html;
3310
+ }
3311
+
3312
+ function renderResultCard(result, index) {
3313
+ var summary = result.summary || {};
3314
+ var context = result.context;
3315
+ var full = result.full;
3316
+ var pct = Math.round((result.score || 0) * 100);
3317
+ var sc = scoreColor(result.score || 0);
3318
+ var scCls = scoreClass(result.score || 0);
3319
+
3320
+ var hasContext = context && (
3321
+ (context.interfaces && context.interfaces.length > 0) ||
3322
+ (context.keyImports && context.keyImports.length > 0) ||
3323
+ (context.relatedConcepts && context.relatedConcepts.length > 0) ||
3324
+ context.usage
3325
+ );
3326
+
3327
+ var hasFull = full && (
3328
+ (full.completeCode && full.completeCode.length > 0) ||
3329
+ (full.documentation && full.documentation.length > 0)
3330
+ );
3331
+
3332
+ var locParts = (summary.location || '').split(':');
3333
+ var fileName = locParts[0] || '';
3334
+ var lineNum = locParts[1] || '';
3335
+
3336
+ var html =
3337
+ '<div class="result-card">' +
3338
+ '<div class="result-header">' +
3339
+ '<div class="result-rank">' + (index + 1) + '</div>' +
3340
+ '<div class="result-main">' +
3341
+ '<div class="result-title-row">' +
3342
+ (summary.type ? resultTypeIcon(summary.type) : '') +
3343
+ '<span class="result-name">' + esc(summary.name || 'Unknown') + '</span>' +
3344
+ '<div class="score-bar-container">' +
3345
+ '<div class="score-bar-track"><div class="score-bar-fill" style="width:' + pct + '%;background:' + sc + '"></div></div>' +
3346
+ '<span class="score-label score-' + scCls + '">' + pct + '%</span>' +
3347
+ '</div>' +
3348
+ '</div>';
3349
+
3350
+ if (summary.signature) {
3351
+ html += '<div class="result-signature">' + esc(summary.signature) + '</div>';
3352
+ }
3353
+ if (summary.purpose) {
3354
+ html += '<p class="result-purpose">' + esc(summary.purpose) + '</p>';
3355
+ }
3356
+
3357
+ html += '<div class="result-meta meta-row">';
3358
+ if (fileName) {
3359
+ html += '<span class="result-location mono" title="' + esc(summary.location || '') + '">' + esc(fileName) + (lineNum ? '<span class="line-number">:' + esc(lineNum) + '</span>' : '') + '</span>';
3360
+ }
3361
+ if (summary.storeName) {
3362
+ html += '<span class="meta-separator"></span><span class="result-store">' + esc(summary.storeName) + '</span>';
3363
+ }
3364
+ html += '</div>';
3365
+
3366
+ if (summary.relevanceReason) {
3367
+ html += '<div class="result-relevance">' + esc(summary.relevanceReason) + '</div>';
3368
+ }
3369
+
3370
+ html += '</div></div>'; // close result-main, result-header
3371
+
3372
+ // Expandable details
3373
+ if (hasContext || hasFull) {
3374
+ var detailLabel = (hasContext && hasFull) ? 'Context &amp; Source Code' : hasContext ? 'Context' : 'Source Code';
3375
+ html += '<details><summary>' + detailLabel + '</summary><div class="details-content">';
3376
+
3377
+ if (hasContext) {
3378
+ html += renderContextSection(context);
3379
+ }
3380
+ if (hasFull) {
3381
+ html += renderFullSection(full);
3382
+ }
3383
+
3384
+ html += '</div></details>';
3385
+ }
3386
+
3387
+ html += '</div>'; // close result-card
3388
+ return html;
3389
+ }
3390
+
3391
+ function renderContextSection(ctx) {
3392
+ var html = '';
3393
+
3394
+ if (ctx.interfaces && ctx.interfaces.length > 0) {
3395
+ html += '<div class="context-group"><div class="section-label">Interfaces</div><ul class="pill-list">';
3396
+ for (var i = 0; i < ctx.interfaces.length; i++) html += '<li class="pill">' + esc(ctx.interfaces[i]) + '</li>';
3397
+ html += '</ul></div>';
3398
+ }
3399
+
3400
+ if (ctx.keyImports && ctx.keyImports.length > 0) {
3401
+ html += '<div class="context-group"><div class="section-label">Key Imports</div><ul class="pill-list">';
3402
+ for (var j = 0; j < ctx.keyImports.length; j++) html += '<li class="pill">' + esc(ctx.keyImports[j]) + '</li>';
3403
+ html += '</ul></div>';
3404
+ }
3405
+
3406
+ if (ctx.relatedConcepts && ctx.relatedConcepts.length > 0) {
3407
+ html += '<div class="context-group"><div class="section-label">Related Concepts</div><ul class="pill-list">';
3408
+ for (var k = 0; k < ctx.relatedConcepts.length; k++) html += '<li class="pill">' + esc(ctx.relatedConcepts[k]) + '</li>';
3409
+ html += '</ul></div>';
3410
+ }
3411
+
3412
+ if (ctx.usage) {
3413
+ html +=
3414
+ '<div class="context-group"><div class="section-label">Usage</div>' +
3415
+ '<div class="usage-stats">' +
3416
+ '<span class="usage-stat"><span class="usage-value">' + ctx.usage.calledBy + '</span><span class="usage-label">called by</span></span>' +
3417
+ '<span class="usage-stat"><span class="usage-value">' + ctx.usage.calls + '</span><span class="usage-label">calls</span></span>' +
3418
+ '</div></div>';
3419
+ }
3420
+
3421
+ return html;
3422
+ }
3423
+
3424
+ function renderFullSection(full) {
3425
+ var html = '';
3426
+
3427
+ if (full.documentation && full.documentation.length > 0) {
3428
+ html += '<div class="full-group"><div class="section-label">Documentation</div><div class="documentation-text">' + esc(full.documentation) + '</div></div>';
3429
+ }
3430
+
3431
+ if (full.completeCode && full.completeCode.length > 0) {
3432
+ html += '<div class="full-group"><div class="section-label">Source Code</div><pre class="code-block"><code>' + esc(full.completeCode) + '</code></pre></div>';
3433
+ }
3434
+
3435
+ return html;
3436
+ }
3437
+
3438
+ // \u2500\u2500\u2500 Bootstrap \u2500\u2500\u2500
3439
+
3440
+ window.addEventListener('hashchange', renderApp);
3441
+ renderApp();
3442
+
3443
+ })();
3444
+ </script>
3445
+ </body>
3446
+ </html>`;
3447
+ }
3448
+
3449
+ // src/server/app.ts
1383
3450
  var CreateStoreBodySchema = z.object({
1384
3451
  name: z.string().min(1, "Store name must be a non-empty string"),
1385
3452
  type: z.enum(["file", "repo", "web"]),
@@ -1413,6 +3480,7 @@ var SearchBodySchema = z.object({
1413
3480
  function createApp(services) {
1414
3481
  const app = new Hono();
1415
3482
  app.use("*", cors());
3483
+ app.get("/", (c) => c.html(renderAdminUI()));
1416
3484
  app.get("/health", (c) => c.json({ status: "ok" }));
1417
3485
  app.get("/api/stores", async (c) => {
1418
3486
  const stores = await services.store.list();
@@ -1425,10 +3493,30 @@ function createApp(services) {
1425
3493
  return c.json({ error: parseResult.error.issues[0]?.message ?? "Invalid request body" }, 400);
1426
3494
  }
1427
3495
  const result = await services.store.create(parseResult.data);
1428
- if (result.success) {
1429
- return c.json(result.data, 201);
3496
+ if (!result.success) {
3497
+ return c.json({ error: result.error.message }, 400);
1430
3498
  }
1431
- return c.json({ error: result.error.message }, 400);
3499
+ const dataDir = services.config.resolveDataDir();
3500
+ const isUrl = parseResult.data.url !== void 0;
3501
+ const jobType = parseResult.data.type === "web" ? "crawl" : parseResult.data.type === "repo" && isUrl ? "clone" : "index";
3502
+ const storePath = result.data.type !== "web" ? result.data.path : void 0;
3503
+ const jobDetails = {
3504
+ storeName: result.data.name,
3505
+ storeId: result.data.id,
3506
+ ...isUrl ? { url: parseResult.data.url } : {},
3507
+ ...storePath !== void 0 ? { path: storePath } : {},
3508
+ phase: jobType === "crawl" ? "crawling" : jobType === "clone" ? "cloning" : "indexing",
3509
+ phaseStep: 1,
3510
+ phaseTotalSteps: jobType === "index" ? 1 : 2
3511
+ };
3512
+ const jobService = new JobService(dataDir);
3513
+ const job = jobService.createJob({
3514
+ type: jobType,
3515
+ details: jobDetails,
3516
+ message: `${jobType === "crawl" ? "Crawling" : "Indexing"} ${result.data.name}...`
3517
+ });
3518
+ spawnBackgroundWorker(job.id, dataDir);
3519
+ return c.json({ ...result.data, job: { id: job.id, status: job.status } }, 201);
1432
3520
  });
1433
3521
  app.get("/api/stores/:id", async (c) => {
1434
3522
  const store = await services.store.getByIdOrName(c.req.param("id"));
@@ -1481,6 +3569,21 @@ function createApp(services) {
1481
3569
  const results = await services.search.search(query);
1482
3570
  return c.json(results);
1483
3571
  });
3572
+ app.get("/api/stores/:id/documents", async (c) => {
3573
+ const store = await services.store.getByIdOrName(c.req.param("id"));
3574
+ if (!store) return c.json({ error: "Not found" }, 404);
3575
+ const limit = parseInt(c.req.query("limit") ?? "50", 10);
3576
+ const offset = parseInt(c.req.query("offset") ?? "0", 10);
3577
+ try {
3578
+ services.lance.setDimensions(await services.embeddings.ensureDimensions());
3579
+ await services.lance.initialize(store.id);
3580
+ const count = await services.lance.countDocuments(store.id);
3581
+ const documents = await services.lance.getAllDocuments(store.id, { limit, offset });
3582
+ return c.json({ documents, total: count, limit, offset });
3583
+ } catch {
3584
+ return c.json({ documents: [], total: 0, limit, offset });
3585
+ }
3586
+ });
1484
3587
  app.post("/api/stores/:id/index", async (c) => {
1485
3588
  const store = await services.store.getByIdOrName(c.req.param("id"));
1486
3589
  if (!store) return c.json({ error: "Not found" }, 404);
@@ -1490,15 +3593,44 @@ function createApp(services) {
1490
3593
  if (result.success) return c.json(result.data);
1491
3594
  return c.json({ error: result.error.message }, 400);
1492
3595
  });
3596
+ app.get("/api/jobs", (c) => {
3597
+ const dataDir = services.config.resolveDataDir();
3598
+ const jobService = new JobService(dataDir);
3599
+ return c.json(jobService.listJobs());
3600
+ });
3601
+ app.get("/api/jobs/:id", (c) => {
3602
+ const dataDir = services.config.resolveDataDir();
3603
+ const jobService = new JobService(dataDir);
3604
+ const job = jobService.getJob(c.req.param("id"));
3605
+ if (!job) return c.json({ error: "Not found" }, 404);
3606
+ return c.json(job);
3607
+ });
1493
3608
  return app;
1494
3609
  }
1495
3610
 
1496
3611
  // src/cli/commands/serve.ts
3612
+ function startServer(fetch2, preferredPort, host) {
3613
+ return new Promise((resolve, reject) => {
3614
+ const server = serve({ fetch: fetch2, port: preferredPort, hostname: host }, (info) => {
3615
+ resolve({ server, port: info.port });
3616
+ });
3617
+ server.on("error", (err2) => {
3618
+ if (err2.code === "EADDRINUSE") {
3619
+ const retryServer = serve({ fetch: fetch2, port: 0, hostname: host }, (info) => {
3620
+ resolve({ server: retryServer, port: info.port });
3621
+ });
3622
+ retryServer.on("error", reject);
3623
+ } else {
3624
+ reject(err2);
3625
+ }
3626
+ });
3627
+ });
3628
+ }
1497
3629
  function createServeCommand(getOptions) {
1498
3630
  return new Command6("serve").description("Start HTTP API server for programmatic search access").option("-p, --port <port>", "Port to listen on (reads from config if not specified)").option(
1499
3631
  "--host <host>",
1500
3632
  "Bind address (reads from config if not specified, use 0.0.0.0 for all interfaces)"
1501
- ).action(async (options) => {
3633
+ ).option("--open", "Open the admin UI in the default browser after starting").action(async (options) => {
1502
3634
  const globalOpts = getOptions();
1503
3635
  const services = await createServices(
1504
3636
  globalOpts.config,
@@ -1507,22 +3639,26 @@ function createServeCommand(getOptions) {
1507
3639
  );
1508
3640
  const appConfig = await services.config.load();
1509
3641
  const app = createApp(services);
1510
- let port;
3642
+ let preferredPort;
1511
3643
  if (options.port !== void 0) {
1512
- port = parseInt(options.port, 10);
1513
- if (Number.isNaN(port)) {
3644
+ preferredPort = parseInt(options.port, 10);
3645
+ if (Number.isNaN(preferredPort)) {
1514
3646
  throw new Error(`Invalid value for --port: "${options.port}" is not a valid integer`);
1515
3647
  }
1516
3648
  } else {
1517
- port = appConfig.server.port;
3649
+ preferredPort = appConfig.server.port;
1518
3650
  }
1519
3651
  const host = options.host ?? appConfig.server.host;
1520
- console.log(`Starting server on http://${host}:${String(port)}`);
1521
- const server = serve({
1522
- fetch: app.fetch,
1523
- port,
1524
- hostname: host
1525
- });
3652
+ const { server, port } = await startServer(app.fetch, preferredPort, host);
3653
+ if (port !== preferredPort) {
3654
+ console.log(`Port ${String(preferredPort)} in use, using ${String(port)}`);
3655
+ }
3656
+ const url = `http://${host}:${String(port)}`;
3657
+ console.log(`Starting server on ${url}`);
3658
+ if (options.open === true) {
3659
+ const openCmd = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
3660
+ exec(`${openCmd} ${url}`);
3661
+ }
1526
3662
  let shuttingDown = false;
1527
3663
  const shutdown = () => {
1528
3664
  if (shuttingDown) return;