@omniradiology/omnirad 0.1.3

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.
Files changed (155) hide show
  1. package/README.md +438 -0
  2. package/app/api/ai-config/route.ts +131 -0
  3. package/app/api/ai-config/test/route.ts +49 -0
  4. package/app/api/auth/auto-login/route.ts +66 -0
  5. package/app/api/auth/check/route.ts +17 -0
  6. package/app/api/auth/login/route.ts +72 -0
  7. package/app/api/auth/logout/route.ts +25 -0
  8. package/app/api/auth/me/route.ts +75 -0
  9. package/app/api/auth/password/route.ts +49 -0
  10. package/app/api/auth/setup/route.ts +63 -0
  11. package/app/api/auth/users/route.ts +100 -0
  12. package/app/api/auth/wipe/route.ts +27 -0
  13. package/app/api/compliance/anonymize/patient/[id]/route.ts +104 -0
  14. package/app/api/compliance/audit/route.ts +110 -0
  15. package/app/api/compliance/export/patient/[id]/route.ts +108 -0
  16. package/app/api/compliance/restrict/patient/[id]/route.ts +59 -0
  17. package/app/api/compliance/settings/route.ts +93 -0
  18. package/app/api/copilot/annotate/route.ts +94 -0
  19. package/app/api/copilot/chat/route.ts +238 -0
  20. package/app/api/copilot/history/route.ts +95 -0
  21. package/app/api/copilot/reports/route.ts +81 -0
  22. package/app/api/fhir/Bundle/report/[id]/route.ts +85 -0
  23. package/app/api/fhir/DiagnosticReport/[id]/route.ts +45 -0
  24. package/app/api/fhir/ImagingStudy/[id]/route.ts +57 -0
  25. package/app/api/fhir/Patient/[id]/route.ts +26 -0
  26. package/app/api/fhir/ServiceRequest/route.ts +85 -0
  27. package/app/api/fhir/config/route.ts +102 -0
  28. package/app/api/fhir/config/test-connection/route.ts +49 -0
  29. package/app/api/fhir/metadata/route.ts +51 -0
  30. package/app/api/pacs/metadata/route.ts +32 -0
  31. package/app/api/pacs/qido/instances/route.ts +39 -0
  32. package/app/api/pacs/qido/series/route.ts +38 -0
  33. package/app/api/pacs/qido/studies/route.ts +37 -0
  34. package/app/api/pacs/test/route.ts +30 -0
  35. package/app/api/pacs/wado/render/route.ts +51 -0
  36. package/app/api/patients/[id]/reports/route.ts +18 -0
  37. package/app/api/patients/[id]/route.ts +43 -0
  38. package/app/api/patients/merge/route.ts +57 -0
  39. package/app/api/patients/route.ts +67 -0
  40. package/app/api/patients/search/route.ts +25 -0
  41. package/app/api/reports/[id]/route.ts +84 -0
  42. package/app/api/reports/[id]/status/route.ts +87 -0
  43. package/app/api/reports/clear/route.ts +16 -0
  44. package/app/api/reports/route.ts +112 -0
  45. package/app/api/segmentation-config/route.ts +238 -0
  46. package/app/api/settings/route.ts +245 -0
  47. package/app/api/settings/test-supabase/route.ts +103 -0
  48. package/app/api/upload/route.ts +48 -0
  49. package/app/copilot/page.tsx +30 -0
  50. package/app/globals.css +141 -0
  51. package/app/history/page.tsx +242 -0
  52. package/app/icon.svg +3 -0
  53. package/app/layout.tsx +47 -0
  54. package/app/login/page.tsx +175 -0
  55. package/app/pacs/page.tsx +78 -0
  56. package/app/page.tsx +125 -0
  57. package/app/patients/[id]/page.tsx +315 -0
  58. package/app/patients/page.tsx +110 -0
  59. package/app/profile/page.tsx +208 -0
  60. package/app/reports/page.tsx +432 -0
  61. package/app/settings/page.tsx +454 -0
  62. package/app/setup/page.tsx +199 -0
  63. package/components/admin/AuditLogTable.tsx +293 -0
  64. package/components/copilot/ActivityIndicator.tsx +215 -0
  65. package/components/copilot/ChatHistoryPanel.tsx +140 -0
  66. package/components/copilot/ChatMessage.tsx +251 -0
  67. package/components/copilot/ClickableReference.tsx +40 -0
  68. package/components/copilot/CopilotCornerstoneViewer.tsx +562 -0
  69. package/components/copilot/CopilotPanel.tsx +311 -0
  70. package/components/copilot/FindingsList.tsx +75 -0
  71. package/components/copilot/ViewerPanel.tsx +460 -0
  72. package/components/copilot/WorkspaceLayout.tsx +398 -0
  73. package/components/dashboard/AIConfigPanel.tsx +339 -0
  74. package/components/dashboard/AppearancePanel.tsx +491 -0
  75. package/components/dashboard/ApprovalModal.tsx +163 -0
  76. package/components/dashboard/CollaborationPanel.tsx +134 -0
  77. package/components/dashboard/CopilotConfigPanel.tsx +337 -0
  78. package/components/dashboard/DicomViewer.tsx +645 -0
  79. package/components/dashboard/FhirIntegrationPanel.tsx +331 -0
  80. package/components/dashboard/FullReportOverlay.tsx +269 -0
  81. package/components/dashboard/ImageViewer.tsx +541 -0
  82. package/components/dashboard/PatientForm.tsx +597 -0
  83. package/components/dashboard/RejectionModal.tsx +74 -0
  84. package/components/dashboard/ReportEditor.tsx +160 -0
  85. package/components/dashboard/ReportTemplates.tsx +729 -0
  86. package/components/dashboard/ReportView.tsx +539 -0
  87. package/components/dashboard/SegmentationConfigPanel.tsx +490 -0
  88. package/components/dashboard/StudyPlaceholder.tsx +17 -0
  89. package/components/dashboard/SupabaseIntegrationPanel.tsx +345 -0
  90. package/components/dashboard/UserManagementPanel.tsx +272 -0
  91. package/components/layout/ClientLayout.tsx +39 -0
  92. package/components/layout/Header.tsx +20 -0
  93. package/components/layout/Sidebar.tsx +119 -0
  94. package/components/pacs/PacsImageViewerModal.tsx +121 -0
  95. package/components/pacs/PacsSearchFilters.tsx +117 -0
  96. package/components/pacs/PacsSeriesViewer.tsx +190 -0
  97. package/components/pacs/PacsStudyTable.tsx +113 -0
  98. package/components/patients/patient-card.tsx +117 -0
  99. package/components/patients/patient-header.tsx +122 -0
  100. package/components/patients/patient-search.tsx +137 -0
  101. package/components/patients/patient-timeline.tsx +153 -0
  102. package/components/settings/ComplianceSettingsPanel.tsx +278 -0
  103. package/components/settings/SecurityPanel.tsx +418 -0
  104. package/components/ui/badge.tsx +19 -0
  105. package/components/ui/basic.tsx +156 -0
  106. package/db/index.ts +350 -0
  107. package/db/migrations/0000_odd_quasimodo.sql +117 -0
  108. package/db/migrations/meta/0000_snapshot.json +778 -0
  109. package/db/migrations/meta/_journal.json +13 -0
  110. package/db/schema.ts +239 -0
  111. package/drizzle.config.ts +10 -0
  112. package/lib/api.ts +689 -0
  113. package/lib/auth.ts +22 -0
  114. package/lib/copilot/action-executor.ts +94 -0
  115. package/lib/copilot/action-types.ts +72 -0
  116. package/lib/copilot/coordinate-mapper.ts +84 -0
  117. package/lib/dicomImageExtractor.ts +103 -0
  118. package/lib/dicomMetadataParser.ts +111 -0
  119. package/lib/fhir/client.ts +25 -0
  120. package/lib/fhir/constants.ts +21 -0
  121. package/lib/fhir/diagnostic-report.ts +88 -0
  122. package/lib/fhir/helpers.ts +73 -0
  123. package/lib/fhir/imaging-study.ts +49 -0
  124. package/lib/fhir/patient.ts +55 -0
  125. package/lib/fhir/service-request.ts +85 -0
  126. package/lib/fhir.ts +6 -0
  127. package/lib/pacs/dicom-utils.ts +72 -0
  128. package/lib/pacs/dicomweb.ts +72 -0
  129. package/lib/pacs/server-utils.ts +37 -0
  130. package/lib/patients.ts +25 -0
  131. package/lib/pdfHelper.ts +119 -0
  132. package/lib/reportHtmlGenerator.ts +581 -0
  133. package/lib/security/audit.ts +180 -0
  134. package/lib/security/authz.ts +246 -0
  135. package/lib/security/phi-redaction.ts +156 -0
  136. package/lib/security/rate-limit.ts +106 -0
  137. package/lib/security/secrets.ts +179 -0
  138. package/lib/supabase.ts +72 -0
  139. package/lib/utils.ts +6 -0
  140. package/next.config.ts +35 -0
  141. package/package.json +76 -0
  142. package/public/file.svg +1 -0
  143. package/public/globe.svg +1 -0
  144. package/public/logo.svg +8 -0
  145. package/public/next.svg +1 -0
  146. package/public/omnirad-favicon.svg +8 -0
  147. package/public/vercel.svg +1 -0
  148. package/public/window.svg +1 -0
  149. package/tsconfig.json +34 -0
  150. package/types/copilot-viewer.ts +155 -0
  151. package/types/copilot.ts +105 -0
  152. package/types/fhir.ts +21 -0
  153. package/types/html2pdf.d.ts +20 -0
  154. package/types/index.ts +139 -0
  155. package/types/pacs.ts +41 -0
@@ -0,0 +1,179 @@
1
+ /**
2
+ * OmniRad Secret Management Module
3
+ *
4
+ * Handles encryption, hashing, masking, and secure storage of secrets
5
+ * like API keys, PACS passwords, and integration tokens.
6
+ */
7
+ import { createCipheriv, createDecipheriv, randomBytes, createHash } from "crypto";
8
+ import path from "path";
9
+ import fs from "fs";
10
+
11
+ // ─── Encryption Key Management ──────────────────────────────────────────────
12
+
13
+ const ALGORITHM = "aes-256-gcm";
14
+ const KEY_LENGTH = 32; // 256 bits
15
+ const IV_LENGTH = 12; // 96 bits for GCM
16
+ const AUTH_TAG_LENGTH = 16;
17
+
18
+ /**
19
+ * Gets or creates the master encryption key.
20
+ * Priority: 1) OMNIRAD_ENCRYPTION_KEY env var, 2) Auto-generated key file
21
+ */
22
+ function getEncryptionKey(): Buffer {
23
+ // Check environment variable first
24
+ const envKey = process.env.OMNIRAD_ENCRYPTION_KEY;
25
+ if (envKey) {
26
+ // If it's hex-encoded
27
+ if (/^[0-9a-fA-F]{64}$/.test(envKey)) {
28
+ return Buffer.from(envKey, "hex");
29
+ }
30
+ // Hash the env key to get exactly 32 bytes
31
+ return createHash("sha256").update(envKey).digest();
32
+ }
33
+
34
+ // Auto-generate and store key in data directory
35
+ const dataDir = path.join(process.cwd(), "data");
36
+ const keyFile = path.join(dataDir, ".encryption_key");
37
+
38
+ try {
39
+ if (fs.existsSync(keyFile)) {
40
+ const keyHex = fs.readFileSync(keyFile, "utf-8").trim();
41
+ return Buffer.from(keyHex, "hex");
42
+ }
43
+ } catch {
44
+ // File doesn't exist or can't be read — generate new key
45
+ }
46
+
47
+ // Generate new key
48
+ const newKey = randomBytes(KEY_LENGTH);
49
+
50
+ // Ensure data directory exists
51
+ if (!fs.existsSync(dataDir)) {
52
+ fs.mkdirSync(dataDir, { recursive: true });
53
+ }
54
+
55
+ // Write key file with restrictive permissions
56
+ fs.writeFileSync(keyFile, newKey.toString("hex"), { mode: 0o600 });
57
+
58
+ return newKey;
59
+ }
60
+
61
+ // Cache the key in memory
62
+ let _cachedKey: Buffer | null = null;
63
+ function getCachedKey(): Buffer {
64
+ if (!_cachedKey) {
65
+ _cachedKey = getEncryptionKey();
66
+ }
67
+ return _cachedKey;
68
+ }
69
+
70
+ // ─── Encryption / Decryption ─────────────────────────────────────────────────
71
+
72
+ /**
73
+ * Encrypts a plaintext string using AES-256-GCM.
74
+ * Returns a string in format: iv:authTag:ciphertext (all hex-encoded)
75
+ */
76
+ export function encryptSecret(plaintext: string): string {
77
+ if (!plaintext) return "";
78
+
79
+ const key = getCachedKey();
80
+ const iv = randomBytes(IV_LENGTH);
81
+ const cipher = createCipheriv(ALGORITHM, key, iv);
82
+
83
+ let encrypted = cipher.update(plaintext, "utf8", "hex");
84
+ encrypted += cipher.final("hex");
85
+ const authTag = cipher.getAuthTag();
86
+
87
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
88
+ }
89
+
90
+ /**
91
+ * Decrypts a ciphertext string encrypted by encryptSecret.
92
+ * Returns the original plaintext.
93
+ */
94
+ export function decryptSecret(ciphertext: string): string {
95
+ if (!ciphertext) return "";
96
+
97
+ // If it doesn't look encrypted (no colons), return as-is for backward compat
98
+ if (!ciphertext.includes(":")) return ciphertext;
99
+
100
+ try {
101
+ const parts = ciphertext.split(":");
102
+ if (parts.length !== 3) return ciphertext; // Not encrypted format
103
+
104
+ const [ivHex, authTagHex, encryptedHex] = parts;
105
+ const key = getCachedKey();
106
+ const iv = Buffer.from(ivHex, "hex");
107
+ const authTag = Buffer.from(authTagHex, "hex");
108
+
109
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
110
+ decipher.setAuthTag(authTag);
111
+
112
+ let decrypted = decipher.update(encryptedHex, "hex", "utf8");
113
+ decrypted += decipher.final("utf8");
114
+
115
+ return decrypted;
116
+ } catch {
117
+ // If decryption fails, return as-is (might be a plaintext from before encryption was added)
118
+ return ciphertext;
119
+ }
120
+ }
121
+
122
+ // ─── Hashing (for verification-only tokens) ──────────────────────────────────
123
+
124
+ /**
125
+ * Creates a SHA-256 hash of a token (for verification-only use cases).
126
+ * Use this for integration tokens where we only need to verify, not recover.
127
+ */
128
+ export function hashToken(token: string): string {
129
+ return createHash("sha256").update(token).digest("hex");
130
+ }
131
+
132
+ /**
133
+ * Verifies a token against its hash.
134
+ */
135
+ export function verifyToken(token: string, hash: string): boolean {
136
+ const computed = hashToken(token);
137
+ // Constant-time comparison to prevent timing attacks
138
+ if (computed.length !== hash.length) return false;
139
+ let result = 0;
140
+ for (let i = 0; i < computed.length; i++) {
141
+ result |= computed.charCodeAt(i) ^ hash.charCodeAt(i);
142
+ }
143
+ return result === 0;
144
+ }
145
+
146
+ // ─── Masking ─────────────────────────────────────────────────────────────────
147
+
148
+ /**
149
+ * Masks a secret value for display, showing only the last 4 characters.
150
+ * Returns "********last4" format.
151
+ */
152
+ export function maskSecret(value: string | null | undefined): string {
153
+ if (!value || value.length === 0) return "";
154
+ if (value.length <= 4) return "********";
155
+
156
+ const last4 = value.slice(-4);
157
+ return `********${last4}`;
158
+ }
159
+
160
+ /**
161
+ * Returns an object indicating whether a secret is set and its masked value.
162
+ * Use this when building API responses that reference secrets.
163
+ */
164
+ export function secretSummary(value: string | null | undefined): { hasSecret: boolean; maskedValue: string } {
165
+ return {
166
+ hasSecret: !!value && value.length > 0,
167
+ maskedValue: maskSecret(value),
168
+ };
169
+ }
170
+
171
+ // ─── Token Generation ────────────────────────────────────────────────────────
172
+
173
+ /**
174
+ * Generates a cryptographically secure random token.
175
+ * Default length is 48 bytes (96 hex chars).
176
+ */
177
+ export function generateSecureToken(byteLength: number = 48): string {
178
+ return randomBytes(byteLength).toString("hex");
179
+ }
@@ -0,0 +1,72 @@
1
+
2
+ import { createClient, SupabaseClient } from '@supabase/supabase-js';
3
+
4
+ // Helper to validate a proper HTTP/HTTPS URL
5
+ function isValidHttpUrl(str: string): boolean {
6
+ try {
7
+ const url = new URL(str);
8
+ return url.protocol === 'http:' || url.protocol === 'https:';
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ // Cache: once we fetch the config from the API, we store it here
15
+ let cachedConfig: { supabaseUrl: string; supabaseAnonKey: string } | null = null;
16
+ let cachedClient: SupabaseClient | null = null;
17
+ let configLoaded = false;
18
+
19
+ // Async function to ensure the config is loaded from the SQLite API
20
+ export async function ensureSupabaseConfig() {
21
+ if (configLoaded) return;
22
+ configLoaded = true;
23
+
24
+ try {
25
+ const res = await fetch('/api/settings?type=config');
26
+ if (res.ok) {
27
+ const data = await res.json();
28
+ if (data.supabaseUrl?.trim() && data.supabaseAnonKey?.trim()) {
29
+ cachedConfig = {
30
+ supabaseUrl: data.supabaseUrl.trim(),
31
+ supabaseAnonKey: data.supabaseAnonKey.trim(),
32
+ };
33
+ }
34
+ }
35
+ } catch (e) {
36
+ console.warn("[OmniRad] Could not fetch Supabase config from API:", e);
37
+ }
38
+ }
39
+
40
+ // Synchronous getter — uses cached config or env vars
41
+ export const getSupabaseClient = (): SupabaseClient | null => {
42
+ // If we already have a cached client, return it
43
+ if (cachedClient) return cachedClient;
44
+
45
+ let supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
46
+ let supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
47
+
48
+ // Override with cached config from API (if available)
49
+ if (cachedConfig) {
50
+ supabaseUrl = cachedConfig.supabaseUrl;
51
+ supabaseAnonKey = cachedConfig.supabaseAnonKey;
52
+ }
53
+
54
+ if (supabaseUrl && supabaseAnonKey && isValidHttpUrl(supabaseUrl)) {
55
+ try {
56
+ cachedClient = createClient(supabaseUrl, supabaseAnonKey);
57
+ return cachedClient;
58
+ } catch (e) {
59
+ console.error("[OmniRad] Failed to create Supabase client:", e);
60
+ return null;
61
+ }
62
+ }
63
+
64
+ return null;
65
+ };
66
+
67
+ // Reset cached client (e.g. when settings change)
68
+ export const resetSupabaseClient = () => {
69
+ cachedClient = null;
70
+ cachedConfig = null;
71
+ configLoaded = false;
72
+ };
package/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
package/next.config.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ output: "standalone",
5
+ outputFileTracingRoot: process.cwd(),
6
+ webpack: (config) => {
7
+ config.experiments = {
8
+ ...config.experiments,
9
+ asyncWebAssembly: true,
10
+ layers: true,
11
+ };
12
+ /* Cornerstone3D wasm file loading */
13
+ config.module.rules.push({
14
+ test: /\.wasm$/,
15
+ type: "asset/resource",
16
+ });
17
+
18
+ /* Stub out Node.js built-ins that Cornerstone3D codec decoders reference */
19
+ config.resolve.fallback = {
20
+ ...config.resolve.fallback,
21
+ fs: false,
22
+ path: false,
23
+ crypto: false,
24
+ stream: false,
25
+ os: false,
26
+ http: false,
27
+ https: false,
28
+ zlib: false,
29
+ };
30
+
31
+ return config;
32
+ },
33
+ };
34
+
35
+ export default nextConfig;
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@omniradiology/omnirad",
3
+ "version": "0.1.3",
4
+ "description": "OmniRad — AI-powered radiology platform with DICOM viewer and intelligent reporting",
5
+ "keywords": [
6
+ "radiology",
7
+ "dicom",
8
+ "medical-imaging",
9
+ "ai",
10
+ "healthcare"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "OmniRad Team",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/omniradiology/omnirad.git"
17
+ },
18
+ "files": [
19
+ "app",
20
+ "components",
21
+ "lib",
22
+ "types",
23
+ "public",
24
+ "db",
25
+ "data",
26
+ "next.config.ts",
27
+ "tsconfig.json",
28
+ "drizzle.config.ts"
29
+ ],
30
+ "scripts": {
31
+ "dev": "concurrently -k -n \"NEXT,AI\" -c \"cyan,magenta\" \"npm run dev:next\" \"npm run dev:ai\"",
32
+ "dev:next": "next dev --webpack",
33
+ "dev:ai": "cd ai_service && python -m uv run main.py",
34
+ "build": "next build --webpack",
35
+ "start": "next start",
36
+ "lint": "eslint",
37
+ "postinstall": "node scripts/patch-html2canvas.js"
38
+ },
39
+ "dependencies": {
40
+ "@cornerstonejs/core": "^4.20.0",
41
+ "@cornerstonejs/dicom-image-loader": "^4.20.0",
42
+ "@cornerstonejs/tools": "^4.20.0",
43
+ "@supabase/supabase-js": "^2.95.3",
44
+ "@types/bcryptjs": "^2.4.6",
45
+ "bcryptjs": "^3.0.3",
46
+ "better-sqlite3": "^12.8.0",
47
+ "clsx": "^2.1.1",
48
+ "dicom-parser": "^1.8.21",
49
+ "drizzle-orm": "^0.45.1",
50
+ "fhir-kit-client": "^2.0.2",
51
+ "html2pdf.js": "^0.14.0",
52
+ "lucide-react": "^0.563.0",
53
+ "next": "16.1.6",
54
+ "react": "19.2.3",
55
+ "react-dom": "19.2.3",
56
+ "react-markdown": "^10.1.0",
57
+ "react-resizable-panels": "^4.6.2",
58
+ "react-signature-canvas": "^1.1.0-alpha.2",
59
+ "remark-gfm": "^4.0.1",
60
+ "tailwind-merge": "^3.4.0"
61
+ },
62
+ "devDependencies": {
63
+ "@tailwindcss/postcss": "^4",
64
+ "@types/better-sqlite3": "^7.6.13",
65
+ "@types/fhir": "^0.0.44",
66
+ "@types/node": "^20",
67
+ "@types/react": "^19",
68
+ "@types/react-dom": "^19",
69
+ "concurrently": "^9.2.1",
70
+ "drizzle-kit": "^0.31.10",
71
+ "eslint": "^9",
72
+ "eslint-config-next": "16.1.6",
73
+ "tailwindcss": "^4",
74
+ "typescript": "^5"
75
+ }
76
+ }
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>