This view is limited to 50 files because it contains too many changes. See the raw diff here.
Files changed (50) hide show
  1. .env.example +2 -2
  2. .github/CODE_OF_CONDUCT.md +0 -84
  3. .github/CONTRIBUTING.md +0 -130
  4. .github/ISSUE_TEMPLATE/bug_report.md +0 -123
  5. .github/ISSUE_TEMPLATE/feature_request.md +0 -73
  6. .github/ISSUE_TEMPLATE/security_vulnerability.md +0 -98
  7. .github/PULL_REQUEST_TEMPLATE.md +0 -44
  8. .github/SECURITY.md +0 -135
  9. .github/hf-space-config.yml +0 -11
  10. .github/workflows/ci.yml +0 -33
  11. .github/workflows/deploy-to-hugging-face.yml +0 -18
  12. .github/workflows/on-pull-request-to-main.yml +0 -9
  13. .github/workflows/on-push-to-main.yml +0 -7
  14. .github/workflows/publish-docker-image.yml +0 -39
  15. .github/workflows/reusable-test-lint-ping.yml +0 -25
  16. .gitignore +0 -2
  17. .husky/pre-commit +1 -2
  18. Dockerfile +3 -4
  19. README.md +4 -20
  20. agents.md +0 -176
  21. biome.json +2 -2
  22. client/components/AiResponse/AiModelDownloadAllowanceContent.tsx +2 -2
  23. client/components/AiResponse/AiResponseContent.tsx +4 -9
  24. client/components/AiResponse/AiResponseSection.tsx +3 -16
  25. client/components/AiResponse/ChatHeader.tsx +1 -1
  26. client/components/AiResponse/ChatInputArea.tsx +5 -22
  27. client/components/AiResponse/ChatInterface.tsx +105 -305
  28. client/components/AiResponse/EnableAiResponsePrompt.tsx +37 -42
  29. client/components/AiResponse/FormattedMarkdown.tsx +1 -1
  30. client/components/AiResponse/MarkdownRenderer.tsx +8 -19
  31. client/components/AiResponse/MessageList.tsx +28 -97
  32. client/components/AiResponse/ReasoningSection.tsx +2 -10
  33. client/components/AiResponse/WebLlmModelSelect.tsx +1 -1
  34. client/components/AiResponse/WllamaModelSelect.tsx +1 -1
  35. client/components/AiResponse/hooks/useReasoningContent.test.ts +0 -96
  36. client/components/AiResponse/hooks/useReasoningContent.ts +6 -33
  37. client/components/Analytics/SearchStats.tsx +0 -313
  38. client/components/App/App.tsx +29 -52
  39. client/components/Logs/LogsModal.tsx +3 -3
  40. client/components/Logs/ShowLogsButton.tsx +1 -1
  41. client/components/Pages/AccessPage.tsx +2 -2
  42. client/components/Pages/Main/MainPage.test.tsx +0 -110
  43. client/components/Pages/Main/MainPage.tsx +6 -6
  44. client/components/Pages/Main/Menu/AISettings/AISettingsForm.tsx +6 -3
  45. client/components/Pages/Main/Menu/AISettings/components/AIParameterSlider.tsx +0 -7
  46. client/components/Pages/Main/Menu/AISettings/components/BrowserSettings.tsx +3 -12
  47. client/components/Pages/Main/Menu/AISettings/components/HordeSettings.tsx +2 -12
  48. client/components/Pages/Main/Menu/AISettings/components/OpenAISettings.tsx +2 -21
  49. client/components/Pages/Main/Menu/AISettings/components/SystemPromptInput.tsx +3 -14
  50. client/components/Pages/Main/Menu/AISettings/hooks/useHordeModels.ts +3 -14
.env.example CHANGED
@@ -5,10 +5,10 @@ ACCESS_KEYS=""
5
  ACCESS_KEY_TIMEOUT_HOURS="24"
6
 
7
  # The default model ID for WebLLM with F16 shaders.
8
- WEBLLM_DEFAULT_F16_MODEL_ID="Qwen3-0.6B-q4f16_1-MLC"
9
 
10
  # The default model ID for WebLLM with F32 shaders.
11
- WEBLLM_DEFAULT_F32_MODEL_ID="Qwen3-0.6B-q4f32_1-MLC"
12
 
13
  # The default model ID for Wllama.
14
  WLLAMA_DEFAULT_MODEL_ID="qwen-3-0.6b"
 
5
  ACCESS_KEY_TIMEOUT_HOURS="24"
6
 
7
  # The default model ID for WebLLM with F16 shaders.
8
+ WEBLLM_DEFAULT_F16_MODEL_ID="Qwen3-1.7B-q4f16_1-MLC"
9
 
10
  # The default model ID for WebLLM with F32 shaders.
11
+ WEBLLM_DEFAULT_F32_MODEL_ID="Qwen3-1.7B-q4f32_1-MLC"
12
 
13
  # The default model ID for Wllama.
14
  WLLAMA_DEFAULT_MODEL_ID="qwen-3-0.6b"
.github/CODE_OF_CONDUCT.md DELETED
@@ -1,84 +0,0 @@
1
- # Contributor Code of Conduct
2
-
3
- ## Our Pledge
4
-
5
- We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
-
7
- We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
-
9
- ## Our Standards
10
-
11
- Examples of behavior that contributes to a positive environment for our community include:
12
-
13
- * Demonstrating empathy and kindness toward other people
14
- * Being respectful of differing opinions, viewpoints, and experiences
15
- * Giving and gracefully accepting constructive feedback
16
- * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
- * Focusing on what is best not just for us as individuals, but for the overall community
18
-
19
- Examples of unacceptable behavior include:
20
-
21
- * The use of sexualized language or imagery, and sexual attention or advances of any kind
22
- * Trolling, insulting or derogatory comments, and personal or political attacks
23
- * Public or private harassment
24
- * Publishing others' private information, such as a physical or email address, without their explicit permission
25
- * Other conduct which could reasonably be considered inappropriate in a professional setting
26
-
27
- ## Enforcement Responsibilities
28
-
29
- Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
30
-
31
- Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
32
-
33
- ## Scope
34
-
35
- This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
36
-
37
- ## Enforcement
38
-
39
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at:
40
-
41
- - GitHub: [@felladrin](https://github.com/felladrin)
42
- - Email: (private contact can be provided upon request)
43
-
44
- All complaints will be reviewed and investigated promptly and fairly.
45
-
46
- All community leaders are obligated to respect the privacy and security of the reporter of any incident.
47
-
48
- ## Enforcement Guidelines
49
-
50
- Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
51
-
52
- ### 1. Correction
53
-
54
- **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
55
-
56
- **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
57
-
58
- ### 2. Warning
59
-
60
- **Community Impact**: A violation through a single incident or series of actions.
61
-
62
- **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
63
-
64
- ### 3. Temporary Ban
65
-
66
- **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
67
-
68
- **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
69
-
70
- ### 4. Permanent Ban
71
-
72
- **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
73
-
74
- **Consequence**: A permanent ban from any sort of public interaction within the community.
75
-
76
- ## Attribution
77
-
78
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
79
-
80
- Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
81
-
82
- [homepage]: https://www.contributor-covenant.org
83
-
84
- For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/CONTRIBUTING.md DELETED
@@ -1,130 +0,0 @@
1
- # Contributing to MiniSearch
2
-
3
- First off, thank you for considering contributing to MiniSearch! It's people like you that make MiniSearch such a great tool.
4
-
5
- ## How to Contribute
6
-
7
- ### Reporting Bugs
8
-
9
- - Use the [GitHub issue tracker](https://github.com/felladrin/MiniSearch/issues) to report bugs
10
- - Check if the issue has already been reported before creating a new one
11
- - Provide clear steps to reproduce the issue
12
- - Include your environment details (OS, browser, Docker version if applicable)
13
- - Add screenshots if the issue is UI-related
14
-
15
- ### Suggesting Features
16
-
17
- - Use the [GitHub issue tracker](https://github.com/felladrin/MiniSearch/issues) for feature suggestions
18
- - Clearly describe the feature and why it would be useful
19
- - Consider if it fits with the project's minimalist philosophy
20
- - Provide examples of how you envision the feature working
21
-
22
- ### Development Setup
23
-
24
- 1. Fork the repository and clone it locally
25
- 2. Make sure you have [Docker](https://docs.docker.com/get-docker/) installed
26
- 3. Start the development server:
27
- ```bash
28
- docker compose up
29
- ```
30
- 4. The application will be available at http://localhost:7860
31
- 5. Make your changes
32
- 6. Test your changes thoroughly
33
- 7. Push to your fork and create a pull request
34
-
35
- ### Running Tests
36
-
37
- ```bash
38
- docker compose exec development-server npm run test
39
- ```
40
-
41
- For coverage:
42
- ```bash
43
- docker compose exec development-server npm run test:coverage
44
- ```
45
-
46
- ### Code Quality
47
-
48
- Before submitting a pull request, please run:
49
- ```bash
50
- docker compose exec development-server npm run lint
51
- ```
52
-
53
- This runs:
54
- - Biome (formatting/linting)
55
- - TypeScript (type checking)
56
- - ts-prune (dead code detection)
57
- - jscpd (copy-paste detection)
58
- - dpdm (circular dependency detection)
59
- - Custom architectural linter
60
-
61
- ## Contribution Guidelines
62
-
63
- ### Types of Contributions We're Looking For
64
-
65
- - **Bug fixes**: Always welcome!
66
- - **Documentation improvements**: Better docs help everyone
67
- - **Performance optimizations**: Especially for search and AI features
68
- - **UI/UX improvements**: Keeping the minimalist yet intuitive design
69
- - **New AI model integrations**: Following existing patterns
70
- - **Security enhancements**: Following security best practices
71
-
72
- ### Project Vision
73
-
74
- MiniSearch aims to be a minimalist, privacy-focused web search application with AI assistance. We prioritize:
75
- - Privacy and security
76
- - Simplicity and ease of use
77
- - Cross-platform compatibility
78
- - Efficient resource usage
79
-
80
- ### Getting in Touch
81
-
82
- - For questions about contributions: Use GitHub discussions
83
- - For security issues: See [SECURITY.md](SECURITY.md)
84
- - For general questions: Use GitHub issues
85
-
86
- ## Pull Request Process
87
-
88
- 1. Fork the repository
89
- 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
90
- 3. Make your changes
91
- 4. Add tests if applicable
92
- 5. Ensure all tests pass and linting succeeds
93
- 6. Commit your changes (`git commit -m 'Add some amazing feature'`)
94
- 7. Push to the branch (`git push origin feature/amazing-feature`)
95
- 8. Open a Pull Request
96
-
97
- ### Pull Request Guidelines
98
-
99
- - Keep PRs focused on a single feature or fix
100
- - Write clear commit messages
101
- - Update documentation if needed
102
- - Ensure your code follows the existing style
103
- - Be responsive to feedback and reviews
104
-
105
- ## Development Commands
106
-
107
- ```bash
108
- # Start development server
109
- docker compose up
110
-
111
- # Run tests
112
- docker compose exec development-server npm run test
113
-
114
- # Run linting
115
- docker compose exec development-server npm run lint
116
-
117
- # Build for production
118
- docker compose -f docker-compose.production.yml build
119
-
120
- # View test coverage
121
- docker compose exec development-server npm run test:coverage
122
- ```
123
-
124
- ## Questions?
125
-
126
- Don't hesitate to ask questions! We're here to help you contribute successfully.
127
-
128
- ---
129
-
130
- Thanks again for your interest in contributing to MiniSearch! ๐ŸŽ‰
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/ISSUE_TEMPLATE/bug_report.md DELETED
@@ -1,123 +0,0 @@
1
- ---
2
- name: Bug report
3
- description: Create a report to help us improve
4
- title: "[BUG] "
5
- labels: ["bug"]
6
- assignees: []
7
- body:
8
- - type: markdown
9
- attributes:
10
- value: |
11
- Thanks for taking the time to fill out this bug report! Please provide as much detail as possible.
12
-
13
- - type: textarea
14
- id: bug-description
15
- attributes:
16
- label: Describe the bug
17
- description: A clear and concise description of what the bug is
18
- placeholder: What happened? What did you expect to happen?
19
- validations:
20
- required: true
21
-
22
- - type: textarea
23
- id: steps-to-reproduce
24
- attributes:
25
- label: Steps to reproduce
26
- description: Please provide detailed steps to reproduce the issue
27
- placeholder: |
28
- 1. Go to '...'
29
- 2. Click on '....'
30
- 3. Scroll down to '....'
31
- 4. See error
32
- validations:
33
- required: true
34
-
35
- - type: textarea
36
- id: expected-behavior
37
- attributes:
38
- label: Expected behavior
39
- description: A clear and concise description of what you expected to happen
40
- validations:
41
- required: true
42
-
43
- - type: textarea
44
- id: screenshots
45
- attributes:
46
- label: Screenshots
47
- description: If applicable, add screenshots to help explain your problem
48
- placeholder: Drag and drop images here or paste them
49
-
50
- - type: dropdown
51
- id: os
52
- attributes:
53
- label: Operating System
54
- description: What operating system are you using?
55
- options:
56
- - Windows 11
57
- - Windows 10
58
- - macOS 15.x
59
- - macOS 14.x
60
- - macOS 13.x
61
- - Ubuntu 22.04
62
- - Ubuntu 20.04
63
- - Other Linux
64
- - Other
65
- validations:
66
- required: true
67
-
68
- - type: dropdown
69
- id: browser
70
- attributes:
71
- label: Browser
72
- description: What browser are you using?
73
- options:
74
- - Chrome
75
- - Firefox
76
- - Safari
77
- - Edge
78
- - Other
79
- validations:
80
- required: true
81
-
82
- - type: input
83
- id: version
84
- attributes:
85
- label: MiniSearch version
86
- description: What version of MiniSearch are you using?
87
- placeholder: latest, v1.0.0, etc.
88
- validations:
89
- required: true
90
-
91
- - type: input
92
- id: docker-version
93
- attributes:
94
- label: Docker version (if applicable)
95
- description: What Docker version are you using?
96
- placeholder: e.g., 24.0.7
97
-
98
- - type: textarea
99
- id: additional-context
100
- attributes:
101
- label: Additional context
102
- description: Add any other context about the problem here
103
-
104
- - type: dropdown
105
- id: deployment-type
106
- attributes:
107
- label: Deployment type
108
- description: How are you running MiniSearch?
109
- options:
110
- - Docker image
111
- - Building from source
112
- - Development server
113
- - Other
114
- validations:
115
- required: true
116
-
117
- - type: textarea
118
- id: custom-config
119
- attributes:
120
- label: Custom configuration (if applicable)
121
- description: Any custom configuration you're using
122
- placeholder: Environment variables, custom settings, etc.
123
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/ISSUE_TEMPLATE/feature_request.md DELETED
@@ -1,73 +0,0 @@
1
- ---
2
- name: Feature request
3
- description: Suggest an idea for this project
4
- title: "[FEATURE] "
5
- labels: ["enhancement"]
6
- assignees: []
7
- body:
8
- - type: markdown
9
- attributes:
10
- value: |
11
- Thanks for suggesting a new feature! Please provide as much detail as possible to help us understand your vision.
12
-
13
- - type: textarea
14
- id: problem-description
15
- attributes:
16
- label: Is your feature request related to a problem?
17
- description: A clear and concise description of what the problem is
18
- placeholder: I'm always frustrated when [...]
19
- validations:
20
- required: true
21
-
22
- - type: textarea
23
- id: solution-description
24
- attributes:
25
- label: Describe the solution you'd like
26
- description: A clear and concise description of what you want to happen
27
- validations:
28
- required: true
29
-
30
- - type: textarea
31
- id: alternatives
32
- attributes:
33
- label: Describe alternatives you've considered
34
- description: A clear and concise description of any alternative solutions or features you've considered
35
-
36
- - type: textarea
37
- id: additional-context
38
- attributes:
39
- label: Additional context
40
- description: Add any other context or screenshots about the feature request here
41
-
42
- - type: dropdown
43
- id: alignment
44
- attributes:
45
- label: Alignment with MiniSearch's goals
46
- description: How does this feature align with MiniSearch's minimalist, privacy-focused philosophy?
47
- options:
48
- - "Strongly aligns - enhances privacy/minimalism"
49
- - "Moderately aligns - useful but adds complexity"
50
- - "Needs consideration - potential trade-offs"
51
- - "Not sure - need discussion"
52
- validations:
53
- required: true
54
-
55
- - type: textarea
56
- id: implementation-ideas
57
- attributes:
58
- label: Implementation ideas (optional)
59
- description: Do you have any ideas about how this could be implemented?
60
- placeholder: Technical thoughts, potential approaches, etc.
61
-
62
- - type: dropdown
63
- id: priority
64
- attributes:
65
- label: Priority (your assessment)
66
- description: How important is this feature to you?
67
- options:
68
- - "High - would significantly improve my experience"
69
- - "Medium - nice to have"
70
- - "Low - minor improvement"
71
- validations:
72
- required: true
73
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/ISSUE_TEMPLATE/security_vulnerability.md DELETED
@@ -1,98 +0,0 @@
1
- ---
2
- name: Security Vulnerability
3
- description: Report a security vulnerability (PRIVATE - will be converted to private report)
4
- title: "[SECURITY] "
5
- labels: ["security"]
6
- assignees: []
7
- body:
8
- - type: markdown
9
- attributes:
10
- value: |
11
- # โš ๏ธ IMPORTANT: SECURITY VULNERABILITIES SHOULD NOT BE REPORTED PUBLICLY โš ๏ธ
12
-
13
- This template will create a public issue, but for security vulnerabilities, please use one of these private reporting methods:
14
-
15
- 1. **Preferred**: Use GitHub's [Private Vulnerability Reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability)
16
- 2. **Alternative**: Email the maintainer privately
17
-
18
- If you accidentally created a public issue, please close it and report privately instead.
19
-
20
- - type: checkboxes
21
- id: confirmation
22
- attributes:
23
- label: Confirmation
24
- description: Please confirm you understand the security reporting process
25
- options:
26
- - label: I understand this should be reported privately and will use the methods above
27
- required: true
28
-
29
- - type: dropdown
30
- id: vulnerability-type
31
- attributes:
32
- label: Vulnerability Type
33
- description: What type of security vulnerability is this?
34
- options:
35
- - Cross-site Scripting (XSS)
36
- - Authentication/Authorization Bypass
37
- - Information Disclosure
38
- - Remote Code Execution
39
- - Denial of Service
40
- - Configuration Issue
41
- - Other
42
- validations:
43
- required: true
44
-
45
- - type: dropdown
46
- id: severity
47
- attributes:
48
- label: Severity
49
- description: How severe is this vulnerability?
50
- options:
51
- - Critical
52
- - High
53
- - Medium
54
- - Low
55
- validations:
56
- required: true
57
-
58
- - type: textarea
59
- id: description
60
- attributes:
61
- label: Description
62
- description: A clear description of the security vulnerability
63
- validations:
64
- required: true
65
-
66
- - type: textarea
67
- id: steps-to-reproduce
68
- attributes:
69
- label: Steps to Reproduce
70
- description: Detailed steps to reproduce the vulnerability
71
- validations:
72
- required: true
73
-
74
- - type: textarea
75
- id: impact
76
- attributes:
77
- label: Impact
78
- description: Describe the potential impact of this vulnerability
79
- validations:
80
- required: true
81
-
82
- - type: textarea
83
- id: mitigation
84
- attributes:
85
- label: Mitigation (if known)
86
- description: Any known workarounds or mitigations
87
-
88
- - type: textarea
89
- id: additional-information
90
- attributes:
91
- label: Additional Information
92
- description: Any additional context or information about the vulnerability
93
-
94
- - type: markdown
95
- attributes:
96
- value: |
97
- ## ๐Ÿšจ PLEASE DO NOT SUBMIT THIS PUBLICLY - USE PRIVATE REPORTING METHODS ABOVE ๐Ÿšจ
98
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/PULL_REQUEST_TEMPLATE.md DELETED
@@ -1,44 +0,0 @@
1
- ## Description
2
- Brief description of what this PR changes.
3
-
4
- ## Type of Change
5
- - [ ] Bug fix (non-breaking change that fixes an issue)
6
- - [ ] New feature (non-breaking change that adds functionality)
7
- - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
8
- - [ ] Documentation update
9
- - [ ] Security fix
10
- - [ ] Performance improvement
11
- - [ ] Code quality improvement
12
-
13
- ## Testing
14
- - [ ] I have tested this change locally
15
- - [ ] I have added tests for this change
16
- - [ ] All existing tests pass
17
- - [ ] I have run the linter and it passes (`npm run lint`)
18
-
19
- ## Checklist
20
- - [ ] My code follows the project's coding conventions
21
- - [ ] I have reviewed my own code
22
- - [ ] I have made corresponding changes to the documentation
23
- - [ ] My changes generate no new warnings
24
- - [ ] I have added tests that prove my fix is effective or that my feature works
25
- - [ ] New and existing unit tests pass locally with my changes
26
- - [ ] Any dependent changes have been merged and published in downstream modules
27
-
28
- ## Screenshots (if applicable)
29
- Add screenshots to help explain your changes, especially for UI modifications.
30
-
31
- ## Additional Context
32
- Add any other context about the problem here.
33
-
34
- ## Security Considerations
35
- - [ ] This change does not introduce any security vulnerabilities
36
- - [ ] I have considered the security implications of this change
37
- - [ ] If this change handles user input, it has been properly sanitized/validated
38
-
39
- ## Performance Impact
40
- - [ ] This change does not negatively impact performance
41
- - [ ] I have tested the performance impact of this change
42
-
43
- ## Breaking Changes
44
- If this PR introduces breaking changes, please describe them here and how users should migrate.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/SECURITY.md DELETED
@@ -1,135 +0,0 @@
1
- # Security Policy
2
-
3
- ## Supported Versions
4
-
5
- Only the latest version of MiniSearch receives security updates.
6
-
7
- | Version | Supported |
8
- |---------|------------|
9
- | Latest | โœ… |
10
- | Older | โŒ |
11
-
12
- ## Reporting a Vulnerability
13
-
14
- ### Private Vulnerability Reporting
15
-
16
- We strongly encourage using GitHub's [Private Vulnerability Reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability) feature to report security vulnerabilities.
17
-
18
- **Do not report security vulnerabilities through public issues.**
19
-
20
- ### How to Report
21
-
22
- 1. **Preferred**: Use GitHub's Private Vulnerability Reporting
23
- 2. **Alternative**: Email the maintainer privately at: (contact can be provided upon request)
24
-
25
- When reporting a vulnerability, please include:
26
- - A clear description of the vulnerability
27
- - Steps to reproduce the issue
28
- - Potential impact of the vulnerability
29
- - Any suggested mitigations (if known)
30
-
31
- ### What to Expect
32
-
33
- - We will acknowledge receipt of your report within 48 hours
34
- - We will provide a detailed response within 7 days
35
- - We will work with you to understand and validate the report
36
- - We will coordinate disclosure timing to minimize user risk
37
-
38
- ## Security Scope
39
-
40
- ### In Scope
41
-
42
- - Vulnerabilities in the MiniSearch web application
43
- - Security issues in the Docker container configuration
44
- - Authentication and authorization bypasses
45
- - Cross-site scripting (XSS) vulnerabilities
46
- - Information disclosure issues
47
- - Remote code execution vulnerabilities
48
- - Privilege escalation in the application context
49
-
50
- ### Out of Scope
51
-
52
- - Issues in third-party dependencies (report to respective projects)
53
- - Vulnerabilities in the underlying browser or Node.js runtime
54
- - Physical attacks on infrastructure
55
- - Social engineering attacks
56
- - Denial of service attacks that don't indicate a vulnerability
57
- - Issues requiring physical access to user devices
58
-
59
- ## Threat Model
60
-
61
- ### MiniSearch's Security Boundaries
62
-
63
- MiniSearch is designed as a privacy-focused search application with the following security assumptions:
64
-
65
- **Trust Boundaries:**
66
- - **Browser Environment**: The application runs entirely in the user's browser
67
- - **Server Component**: Optional backend for search and AI processing
68
- - **AI Models**: Local or remote AI processing with configurable endpoints
69
-
70
- **Data Flow:**
71
- - User queries are sent to SearXNG instances (configurable)
72
- - AI processing can be local (WebLLM/Wllama) or remote (API endpoints)
73
- - Search history is stored locally in the browser
74
- - No tracking or analytics by default
75
-
76
- **Security Controls:**
77
- - Optional access key protection for deployment
78
- - Configurable AI endpoints for privacy
79
- - Local-first data storage
80
- - No third-party tracking or analytics
81
-
82
- **Potential Risks:**
83
- - Malicious SearXNG instances could log queries
84
- - Remote AI endpoints could access user queries
85
- - Browser extensions could interfere with the application
86
- - Man-in-the-middle attacks without HTTPS
87
-
88
- ## Security Best Practices
89
-
90
- ### For Users
91
-
92
- - Always use HTTPS when accessing MiniSearch instances
93
- - Configure trusted SearXNG instances
94
- - Use local AI models for maximum privacy
95
- - Set access keys for deployed instances
96
- - Keep browsers updated
97
-
98
- ### For Deployers
99
-
100
- - Use the official Docker image
101
- - Configure environment variables securely
102
- - Set up proper access controls
103
- - Use HTTPS in production
104
- - Regularly update dependencies
105
- - Monitor for security advisories
106
-
107
- ## Security Features
108
-
109
- - **Access Key Protection**: Optional password-based access control
110
- - **Configurable Endpoints**: Users control search and AI providers
111
- - **Local Processing**: AI models can run entirely in the browser
112
- - **No Tracking**: Built without analytics or tracking
113
- - **HTTPS Ready**: Designed for secure deployment
114
-
115
- ## Security Updates
116
-
117
- Security updates will be:
118
- - Released as new versions
119
- - Announced in release notes
120
- - Coordinated with dependency updates when applicable
121
-
122
- ## Security Team
123
-
124
- The MiniSearch security team is currently the project maintainer:
125
- - [@felladrin](https://github.com/felladrin) - Project Maintainer
126
-
127
- ## Acknowledgments
128
-
129
- We thank security researchers who help us keep MiniSearch secure. All valid security reports will be acknowledged in our release notes (with reporter permission).
130
-
131
- ## Related Resources
132
-
133
- - [GitHub Security Advisories](https://docs.github.com/en/code-security/security-advisories)
134
- - [OWASP Web Security Testing Guide](https://owasp.org/www-project-web-security-testing-guide/)
135
- - [Mozilla Security Guidelines](https://infosec.mozilla.org/guidelines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/hf-space-config.yml DELETED
@@ -1,11 +0,0 @@
1
- title: MiniSearch
2
- emoji: ๐Ÿ‘Œ๐Ÿ”
3
- colorFrom: yellow
4
- colorTo: yellow
5
- sdk: docker
6
- short_description: Minimalist web-searching app with browser-based AI assistant
7
- pinned: true
8
- custom_headers:
9
- cross-origin-embedder-policy: require-corp
10
- cross-origin-opener-policy: same-origin
11
- cross-origin-resource-policy: cross-origin
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/ci.yml DELETED
@@ -1,33 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main, master]
6
- pull_request:
7
- branches: [main, master]
8
-
9
- jobs:
10
- build-test:
11
- runs-on: ubuntu-latest
12
-
13
- steps:
14
- - name: Checkout repository
15
- uses: actions/checkout@v6
16
-
17
- - name: Set up Node.js
18
- uses: actions/setup-node@v6
19
- with:
20
- node-version: lts/*
21
- cache: npm
22
-
23
- - name: Install dependencies
24
- run: npm ci
25
-
26
- - name: Run lint
27
- run: npm run lint
28
-
29
- - name: Check formatting
30
- run: npm run format
31
-
32
- - name: Run tests
33
- run: npm test
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/deploy-to-hugging-face.yml DELETED
@@ -1,18 +0,0 @@
1
- name: Deploy to Hugging Face
2
-
3
- on:
4
- workflow_dispatch:
5
-
6
- jobs:
7
- sync-to-hf:
8
- name: Sync to Hugging Face Spaces
9
- runs-on: ubuntu-latest
10
- steps:
11
- - uses: actions/checkout@v6
12
- - uses: JacobLinCool/huggingface-sync@v1
13
- with:
14
- github: ${{ secrets.GITHUB_TOKEN }}
15
- user: ${{ vars.HF_SPACE_OWNER }}
16
- space: ${{ vars.HF_SPACE_NAME }}
17
- token: ${{ secrets.HF_TOKEN }}
18
- configuration: ".github/hf-space-config.yml"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/on-pull-request-to-main.yml DELETED
@@ -1,9 +0,0 @@
1
- name: On Pull Request To Main
2
- on:
3
- pull_request:
4
- types: [opened, synchronize, reopened]
5
- branches: ["main"]
6
- jobs:
7
- test-lint-ping:
8
- if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-test-lint-ping') }}
9
- uses: ./.github/workflows/reusable-test-lint-ping.yml
 
 
 
 
 
 
 
 
 
 
.github/workflows/on-push-to-main.yml DELETED
@@ -1,7 +0,0 @@
1
- name: On Push To Main
2
- on:
3
- push:
4
- branches: ["main"]
5
- jobs:
6
- test-lint-ping:
7
- uses: ./.github/workflows/reusable-test-lint-ping.yml
 
 
 
 
 
 
 
 
.github/workflows/publish-docker-image.yml DELETED
@@ -1,39 +0,0 @@
1
- name: Publish Docker Image
2
-
3
- on:
4
- workflow_dispatch:
5
-
6
- jobs:
7
- build-and-push-image:
8
- name: Publish Docker Image to GitHub Packages
9
- runs-on: ubuntu-latest
10
- env:
11
- REGISTRY: ghcr.io
12
- IMAGE_NAME: ${{ github.repository }}
13
- permissions:
14
- contents: read
15
- packages: write
16
- steps:
17
- - name: Checkout repository
18
- uses: actions/checkout@v6
19
- - name: Log in to the Container registry
20
- uses: docker/login-action@v4
21
- with:
22
- registry: ${{ env.REGISTRY }}
23
- username: ${{ github.actor }}
24
- password: ${{ secrets.GITHUB_TOKEN }}
25
- - name: Extract metadata (tags, labels) for Docker
26
- id: meta
27
- uses: docker/metadata-action@v6
28
- with:
29
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
30
- - name: Set up Docker Buildx
31
- uses: docker/setup-buildx-action@v4
32
- - name: Build and push Docker Image
33
- uses: docker/build-push-action@v7
34
- with:
35
- context: .
36
- push: true
37
- tags: ${{ steps.meta.outputs.tags }}
38
- labels: ${{ steps.meta.outputs.labels }}
39
- platforms: linux/amd64,linux/arm64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/reusable-test-lint-ping.yml DELETED
@@ -1,25 +0,0 @@
1
- on:
2
- workflow_call:
3
- jobs:
4
- check-code-quality:
5
- name: Check Code Quality
6
- runs-on: ubuntu-latest
7
- steps:
8
- - uses: actions/checkout@v6
9
- - uses: actions/setup-node@v6
10
- with:
11
- node-version: "lts/*"
12
- cache: "npm"
13
- - run: npm ci --ignore-scripts
14
- - run: npm run lint
15
- check-docker-container:
16
- needs: [check-code-quality]
17
- name: Check Docker Container
18
- runs-on: ubuntu-latest
19
- steps:
20
- - uses: actions/checkout@v6
21
- - run: docker compose -f docker-compose.production.yml up -d
22
- - name: Check if main page is available
23
- run: until curl -s -o /dev/null -w "%{http_code}" localhost:7860 | grep 200; do sleep 1; done
24
- timeout-minutes: 1
25
- - run: docker compose -f docker-compose.production.yml down
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore CHANGED
@@ -5,5 +5,3 @@ node_modules
5
  .vscode
6
  /vite-build-stats.html
7
  .env
8
- /coverage
9
- .playwright-cli
 
5
  .vscode
6
  /vite-build-stats.html
7
  .env
 
 
.husky/pre-commit CHANGED
@@ -1,2 +1 @@
1
- #!/usr/bin/env sh
2
- npm install --no-save @biomejs/biome && npx @biomejs/biome check --write --staged --no-errors-on-unmatched && git diff --name-only --cached | while read -r file; do git add "$file"; done
 
1
+ npx lint-staged
 
Dockerfile CHANGED
@@ -1,6 +1,6 @@
1
  FROM node:lts AS llama-builder
2
 
3
- ARG LLAMA_CPP_RELEASE_TAG="b6604"
4
 
5
  RUN apt-get update && apt-get install -y \
6
  build-essential \
@@ -55,8 +55,7 @@ RUN cp searx/settings.yml $SEARXNG_SETTINGS_PATH && \
55
  chmod 644 $SEARXNG_SETTINGS_PATH && \
56
  sed -i 's/ultrasecretkey/'$(openssl rand -hex 32)'/g' $SEARXNG_SETTINGS_PATH && \
57
  sed -i 's/- html/- json/' $SEARXNG_SETTINGS_PATH && \
58
- /usr/local/searxng/searxng-venv/bin/pip install -r requirements.txt && \
59
- /usr/local/searxng/searxng-venv/bin/pip install --no-build-isolation -e .
60
 
61
  COPY --from=llama-builder /tmp/llama.cpp/build/bin/llama-server /usr/local/bin/
62
  COPY --from=llama-builder /usr/local/lib/llama/* /usr/local/lib/
@@ -93,4 +92,4 @@ HEALTHCHECK --interval=5m CMD curl -f http://localhost:7860/status || exit 1
93
 
94
  ENTRYPOINT [ "/bin/sh", "-c" ]
95
 
96
- CMD ["(cd /usr/local/searxng/searxng-src && /usr/local/searxng/searxng-venv/bin/python -m searx.webapp > /dev/null 2>&1) & npm start -- --host"]
 
1
  FROM node:lts AS llama-builder
2
 
3
+ ARG LLAMA_CPP_RELEASE_TAG="b5595"
4
 
5
  RUN apt-get update && apt-get install -y \
6
  build-essential \
 
55
  chmod 644 $SEARXNG_SETTINGS_PATH && \
56
  sed -i 's/ultrasecretkey/'$(openssl rand -hex 32)'/g' $SEARXNG_SETTINGS_PATH && \
57
  sed -i 's/- html/- json/' $SEARXNG_SETTINGS_PATH && \
58
+ /usr/local/searxng/searxng-venv/bin/pip install -e .
 
59
 
60
  COPY --from=llama-builder /tmp/llama.cpp/build/bin/llama-server /usr/local/bin/
61
  COPY --from=llama-builder /usr/local/lib/llama/* /usr/local/lib/
 
92
 
93
  ENTRYPOINT [ "/bin/sh", "-c" ]
94
 
95
+ CMD ["(cd /usr/local/searxng/searxng-src && /usr/local/searxng/searxng-venv/bin/python -m searx.webapp > /dev/null 2>&1) & (npx pm2 start ecosystem.config.cjs && npx pm2 logs)" ]
README.md CHANGED
@@ -64,7 +64,7 @@ docker compose -f docker-compose.production.yml up --build
64
 
65
  Once the container is running, open http://localhost:7860 in your browser and start searching!
66
 
67
- ## Frequently asked questions [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/felladrin/MiniSearch)
68
 
69
  <details>
70
  <summary>How do I search via the browser's address bar?</summary>
@@ -121,23 +121,7 @@ Once the container is running, open http://localhost:7860 in your browser and st
121
 
122
  <details>
123
  <summary>How can I contribute to the development of this tool?</summary>
124
- <p>We welcome contributions! Please read our <a href=".github/CONTRIBUTING.md">Contributing Guidelines</a> for detailed information on how to get started.</p>
125
- <p>Quick start:</p>
126
- <ol>
127
- <li>Fork this repository and clone it</li>
128
- <li>Start the development server: <code>docker compose up</code></li>
129
- <li>Make your changes and test them</li>
130
- <li>Push to your fork and open a pull request</li>
131
- </ol>
132
- <p>All contributions are welcome! ๐ŸŽ‰</p>
133
- </details>
134
-
135
- <details>
136
- <summary>Where can I find more information?</summary>
137
- <ul>
138
- <li><a href=".github/CONTRIBUTING.md">Contributing Guidelines</a> - How to contribute to MiniSearch</li>
139
- <li><a href=".github/CODE_OF_CONDUCT.md">Code of Conduct</a> - Our community guidelines</li>
140
- <li><a href=".github/SECURITY.md">Security Policy</a> - How to report security vulnerabilities</li>
141
- <li><a href="docs/">Documentation</a> - Detailed project documentation</li>
142
- </ul>
143
  </details>
 
64
 
65
  Once the container is running, open http://localhost:7860 in your browser and start searching!
66
 
67
+ ## Frequently asked questions
68
 
69
  <details>
70
  <summary>How do I search via the browser's address bar?</summary>
 
121
 
122
  <details>
123
  <summary>How can I contribute to the development of this tool?</summary>
124
+ <p>Fork this repository and clone it. Then, start the development server by running the following command:</p>
125
+ <p><code>docker compose up</code></p>
126
+ <p>Make your changes, push them to your fork, and open a pull request! All contributions are welcome!</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  </details>
agents.md DELETED
@@ -1,176 +0,0 @@
1
- # MiniSearch Agent Guidelines
2
-
3
- This is your navigation hub. Start here, follow the links, and return when you need orientation.
4
-
5
- ## Before You Start
6
-
7
- **New to this codebase?** Read in this order:
8
- 1. `docs/quick-start.md` - Get it running
9
- 2. `docs/overview.md` - Understand the system
10
- 3. `docs/project-structure.md` - Navigate the code
11
-
12
- **Making changes?** Check:
13
- - `docs/coding-conventions.md` - Code style
14
- - `docs/development-commands.md` - Available commands
15
- - `docs/pull-requests.md` - How to submit
16
-
17
- ## Repository Map
18
-
19
- ### Getting Started
20
- - **`docs/quick-start.md`** - Installation, first run, verification
21
- - **`docs/overview.md`** - System architecture and data flow
22
- - **`docs/project-structure.md`** - Directory layout and component organization
23
-
24
- ### Configuration & Setup
25
- - **`docs/configuration.md`** - Environment variables and settings reference
26
- - **`docs/security.md`** - Access control, privacy, and security model
27
-
28
- ### Core Functionality
29
- - **`docs/ai-integration.md`** - AI inference types (WebLLM, Wllama, OpenAI, AI Horde, Internal)
30
- - **`docs/ui-components.md`** - Component architecture and PubSub patterns
31
- - **`docs/search-history.md`** - History database schema and management
32
- - **`docs/conversation-memory.md`** - Token budgeting and rolling summaries
33
-
34
- ### Development
35
- - **`docs/development-commands.md`** - Docker, npm, and testing commands
36
- - **`docs/coding-conventions.md`** - Style guide and patterns
37
- - **`docs/pull-requests.md`** - PR process and merge philosophy
38
- - **`docs/core-technologies.md`** - Technology stack and dependencies
39
- - **`docs/design.md`** - UI/UX design principles
40
-
41
- ## Agent Decision Tree
42
-
43
- ```
44
- Need to:
45
- โ”œโ”€โ”€ Add a feature?
46
- โ”‚ โ”œโ”€โ”€ UI component โ†’ docs/ui-components.md
47
- โ”‚ โ”œโ”€โ”€ AI integration โ†’ docs/ai-integration.md
48
- โ”‚ โ”œโ”€โ”€ Search functionality โ†’ client/modules/search.ts
49
- โ”‚ โ””โ”€โ”€ Settings option โ†’ docs/configuration.md
50
- โ”œโ”€โ”€ Fix a bug?
51
- โ”‚ โ”œโ”€โ”€ UI issue โ†’ Check component + PubSub channels
52
- โ”‚ โ”œโ”€โ”€ AI not working โ†’ docs/ai-integration.md + browser console
53
- โ”‚ โ”œโ”€โ”€ Search failing โ†’ Check SearXNG + server hooks
54
- โ”‚ โ””โ”€โ”€ Build error โ†’ docs/development-commands.md
55
- โ”œโ”€โ”€ Configure deployment?
56
- โ”‚ โ”œโ”€โ”€ Environment variables โ†’ docs/configuration.md
57
- โ”‚ โ”œโ”€โ”€ Access control โ†’ docs/security.md
58
- โ”‚ โ””โ”€โ”€ Docker setup โ†’ docs/overview.md
59
- โ””โ”€โ”€ Understand data flow?
60
- โ”œโ”€โ”€ Search flow โ†’ client/modules/search.ts
61
- โ”œโ”€โ”€ AI generation โ†’ client/modules/textGeneration.ts
62
- โ”œโ”€โ”€ State management โ†’ docs/ui-components.md
63
- โ””โ”€โ”€ History/Chat โ†’ docs/search-history.md + docs/conversation-memory.md
64
- ```
65
-
66
- ## Key Files Reference
67
-
68
- ### Entry Points
69
- - `client/index.tsx` - React app initialization
70
- - `vite.config.ts` - Vite dev server with hooks
71
- - `Dockerfile` - Multi-stage container build
72
-
73
- ### Business Logic Modules
74
- - `client/modules/search.ts` - Search orchestration and caching
75
- - `client/modules/textGeneration.ts` - AI response flow
76
- - `client/modules/pubSub.ts` - All PubSub channels
77
- - `client/modules/settings.ts` - Settings management
78
- - `client/modules/history.ts` - Search history database
79
-
80
- ### Server-Side Modules
81
- - `server/searchEndpointServerHook.ts` - `/search` endpoints
82
- - `server/internalApiEndpointServerHook.ts` - `/inference` proxy
83
- - `server/webSearchService.ts` - SearXNG integration
84
- - `server/rerankerService.ts` - Local result reranking
85
-
86
- ### Key Components
87
- - `client/components/App/` - Application shell
88
- - `client/components/Search/Form/` - Search input
89
- - `client/components/Search/Results/` - Results display
90
- - `client/components/AiResponse/` - AI response + chat
91
- - `client/components/Pages/Main/Menu/` - Settings drawers
92
- - `client/modules/webGpu.ts` - Detects WebGPU availability and F16 shader support for WebLLM
93
- - `client/modules/querySuggestions.ts` - Provides search suggestion UI, stored in IndexedDB
94
- - `client/modules/relatedSearchQuery.ts` - Generates related search queries
95
- - `client/modules/followUpQuestions.ts` - Generates suggested follow-up questions, uses `followUpQuestionPubSub`
96
- - `client/modules/accessKey.ts` - Validates and stores access keys, uses `accessKeyValidatedPubSub`
97
- - `client/modules/parentWindow.ts` - PostMessage API for embedding in parent windows
98
- - `client/hooks/` - Reusable React hooks
99
- - `server/searchToken.ts` - Generates CSRF protection tokens for search requests
100
- - `server/downloadFileFromHuggingFaceRepository.ts` - Downloads GGUF models from HuggingFace using `@huggingface/hub` package
101
-
102
- ## Common Tasks Quick Reference
103
-
104
- ### Add a new AI model
105
- 1. Add to `client/modules/wllama.ts` or WebLLM registry
106
- 2. Update `docs/ai-integration.md`
107
- 3. Update `docs/configuration.md` defaults
108
-
109
- ### Add a new setting
110
- 1. Add to `client/modules/settings.ts` default object
111
- 2. Add UI in `client/components/Pages/Main/Menu/`
112
- 3. Update `docs/configuration.md` settings table
113
-
114
- ### Modify search behavior
115
- 1. Edit `client/modules/search.ts`
116
- 2. Update `server/webSearchService.ts` if server-side changes needed
117
- 3. Check `server/rerankerService.ts` if reranking affected
118
-
119
- ### Fix UI state issues
120
- 1. Check PubSub channels in `client/modules/pubSub.ts`
121
- 2. Verify component subscriptions in `docs/ui-components.md`
122
- 3. Ensure proper state updates in business logic modules
123
-
124
- ### Analyze test coverage
125
- 1. Run `npm run test:coverage` to generate reports
126
- 2. Check `coverage/coverage-summary.json` for quick metrics
127
- 3. See `docs/development-commands.md` for full coverage analysis guide
128
-
129
- ## Quality Gates
130
-
131
- Before any change:
132
- ```bash
133
- docker compose exec development-server npm run lint
134
- ```
135
-
136
- This runs:
137
- - Biome (formatting/linting)
138
- - TypeScript (type checking)
139
- - ts-prune (dead code detection)
140
- - jscpd (copy-paste detection)
141
- - dpdm (circular dependency detection)
142
- - Custom architectural linter
143
-
144
- ## Agent-First Principles
145
-
146
- **Repository as System of Record:**
147
- - All knowledge lives in versioned docs/ structure
148
- - This file is your entry point - start here
149
- - Follow links, don't assume - verify in code
150
-
151
- **Context Efficiency:**
152
- - Use this map to navigate quickly
153
- - Return to this file when context drifts
154
- - Follow the decision tree for common tasks
155
-
156
- **Architecture & Boundaries:**
157
- - Respect PubSub boundaries - don't cross concerns
158
- - Client vs server - keep them separate
159
- - Feature-based organization - one folder per feature
160
-
161
- **Documentation Maintenance:**
162
- - Update these docs when you learn something new
163
- - Add cross-references when linking concepts
164
- - Keep examples current with actual code
165
-
166
- ## Technology Stack
167
-
168
- React + TypeScript + Mantine UI v8, with privacy-first architecture.
169
- See `docs/core-technologies.md` for complete dependency list and selection criteria.
170
-
171
- ## Need Help?
172
-
173
- 1. Check relevant doc in `docs/`
174
- 2. Read the module code in `client/modules/` or `server/`
175
- 3. Look at similar existing implementations
176
- 4. Run `npm run lint` to validate changes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
biome.json CHANGED
@@ -1,9 +1,9 @@
1
  {
2
  "$schema": "https://biomejs.dev/schemas/latest/schema.json",
3
  "vcs": {
4
- "enabled": true,
5
  "clientKind": "git",
6
- "useIgnoreFile": true
7
  },
8
  "files": {
9
  "ignoreUnknown": false
 
1
  {
2
  "$schema": "https://biomejs.dev/schemas/latest/schema.json",
3
  "vcs": {
4
+ "enabled": false,
5
  "clientKind": "git",
6
+ "useIgnoreFile": false
7
  },
8
  "files": {
9
  "ignoreUnknown": false
client/components/AiResponse/AiModelDownloadAllowanceContent.tsx CHANGED
@@ -2,8 +2,8 @@ import { Alert, Button, Group, Text } from "@mantine/core";
2
  import { IconCheck, IconInfoCircle, IconX } from "@tabler/icons-react";
3
  import { usePubSub } from "create-pubsub/react";
4
  import { useState } from "react";
5
- import { addLogEntry } from "@/modules/logEntries";
6
- import { settingsPubSub } from "@/modules/pubSub";
7
 
8
  export default function AiModelDownloadAllowanceContent() {
9
  const [settings, setSettings] = usePubSub(settingsPubSub);
 
2
  import { IconCheck, IconInfoCircle, IconX } from "@tabler/icons-react";
3
  import { usePubSub } from "create-pubsub/react";
4
  import { useState } from "react";
5
+ import { addLogEntry } from "../../modules/logEntries";
6
+ import { settingsPubSub } from "../../modules/pubSub";
7
 
8
  export default function AiModelDownloadAllowanceContent() {
9
  const [settings, setSettings] = usePubSub(settingsPubSub);
client/components/AiResponse/AiResponseContent.tsx CHANGED
@@ -20,9 +20,9 @@ import {
20
  import type { PublishFunction } from "create-pubsub";
21
  import { usePubSub } from "create-pubsub/react";
22
  import { type ReactNode, useMemo, useState } from "react";
23
- import { addLogEntry } from "@/modules/logEntries";
24
- import { settingsPubSub } from "@/modules/pubSub";
25
- import { searchAndRespond } from "@/modules/textGeneration";
26
  import CopyIconButton from "./CopyIconButton";
27
  import FormattedMarkdown from "./FormattedMarkdown";
28
 
@@ -51,12 +51,7 @@ export default function AiResponseContent({
51
  () =>
52
  ({ children }: { children: ReactNode }) => {
53
  return settings.enableAiResponseScrolling ? (
54
- <ScrollArea.Autosize
55
- mah={300}
56
- type="auto"
57
- scrollbars="y"
58
- offsetScrollbars
59
- >
60
  {children}
61
  </ScrollArea.Autosize>
62
  ) : (
 
20
  import type { PublishFunction } from "create-pubsub";
21
  import { usePubSub } from "create-pubsub/react";
22
  import { type ReactNode, useMemo, useState } from "react";
23
+ import { addLogEntry } from "../../modules/logEntries";
24
+ import { settingsPubSub } from "../../modules/pubSub";
25
+ import { searchAndRespond } from "../../modules/textGeneration";
26
  import CopyIconButton from "./CopyIconButton";
27
  import FormattedMarkdown from "./FormattedMarkdown";
28
 
 
51
  () =>
52
  ({ children }: { children: ReactNode }) => {
53
  return settings.enableAiResponseScrolling ? (
54
+ <ScrollArea.Autosize mah={300} type="auto" offsetScrollbars>
 
 
 
 
 
55
  {children}
56
  </ScrollArea.Autosize>
57
  ) : (
client/components/AiResponse/AiResponseSection.tsx CHANGED
@@ -2,16 +2,14 @@ import { CodeHighlightAdapterProvider } from "@mantine/code-highlight";
2
  import { usePubSub } from "create-pubsub/react";
3
  import { useMemo } from "react";
4
  import {
5
- chatMessagesPubSub,
6
- isRestoringFromHistoryPubSub,
7
  modelLoadingProgressPubSub,
8
  modelSizeInMegabytesPubSub,
9
  queryPubSub,
10
  responsePubSub,
11
  settingsPubSub,
12
  textGenerationStatePubSub,
13
- } from "@/modules/pubSub";
14
- import { shikiAdapter } from "@/modules/shiki";
15
  import "@mantine/code-highlight/styles.css";
16
  import AiModelDownloadAllowanceContent from "./AiModelDownloadAllowanceContent";
17
  import AiResponseContent from "./AiResponseContent";
@@ -28,8 +26,6 @@ export default function AiResponseSection() {
28
  const [modelLoadingProgress] = usePubSub(modelLoadingProgressPubSub);
29
  const [settings] = usePubSub(settingsPubSub);
30
  const [modelSizeInMegabytes] = usePubSub(modelSizeInMegabytesPubSub);
31
- const [chatMessages] = usePubSub(chatMessagesPubSub);
32
- const [isRestoringFromHistory] = usePubSub(isRestoringFromHistoryPubSub);
33
 
34
  return useMemo(() => {
35
  if (!settings.enableAiResponse || textGenerationState === "idle") {
@@ -52,14 +48,7 @@ export default function AiResponseSection() {
52
  />
53
 
54
  {textGenerationState === "completed" && (
55
- <ChatInterface
56
- initialQuery={query}
57
- initialResponse={response}
58
- initialMessages={
59
- chatMessages.length > 0 ? chatMessages : undefined
60
- }
61
- suppressInitialFollowUp={isRestoringFromHistory}
62
- />
63
  )}
64
  </CodeHighlightAdapterProvider>
65
  );
@@ -92,10 +81,8 @@ export default function AiResponseSection() {
92
  textGenerationState,
93
  response,
94
  query,
95
- chatMessages,
96
  modelLoadingProgress,
97
  modelSizeInMegabytes,
98
  setTextGenerationState,
99
- isRestoringFromHistory,
100
  ]);
101
  }
 
2
  import { usePubSub } from "create-pubsub/react";
3
  import { useMemo } from "react";
4
  import {
 
 
5
  modelLoadingProgressPubSub,
6
  modelSizeInMegabytesPubSub,
7
  queryPubSub,
8
  responsePubSub,
9
  settingsPubSub,
10
  textGenerationStatePubSub,
11
+ } from "../../modules/pubSub";
12
+ import { shikiAdapter } from "../../modules/shiki";
13
  import "@mantine/code-highlight/styles.css";
14
  import AiModelDownloadAllowanceContent from "./AiModelDownloadAllowanceContent";
15
  import AiResponseContent from "./AiResponseContent";
 
26
  const [modelLoadingProgress] = usePubSub(modelLoadingProgressPubSub);
27
  const [settings] = usePubSub(settingsPubSub);
28
  const [modelSizeInMegabytes] = usePubSub(modelSizeInMegabytesPubSub);
 
 
29
 
30
  return useMemo(() => {
31
  if (!settings.enableAiResponse || textGenerationState === "idle") {
 
48
  />
49
 
50
  {textGenerationState === "completed" && (
51
+ <ChatInterface initialQuery={query} initialResponse={response} />
 
 
 
 
 
 
 
52
  )}
53
  </CodeHighlightAdapterProvider>
54
  );
 
81
  textGenerationState,
82
  response,
83
  query,
 
84
  modelLoadingProgress,
85
  modelSizeInMegabytes,
86
  setTextGenerationState,
 
87
  ]);
88
  }
client/components/AiResponse/ChatHeader.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import { Group, Text } from "@mantine/core";
2
- import type { ChatMessage } from "@/modules/types";
3
  import CopyIconButton from "./CopyIconButton";
4
 
5
  interface ChatHeaderProps {
 
1
  import { Group, Text } from "@mantine/core";
2
+ import type { ChatMessage } from "gpt-tokenizer/GptEncoding";
3
  import CopyIconButton from "./CopyIconButton";
4
 
5
  interface ChatHeaderProps {
client/components/AiResponse/ChatInputArea.tsx CHANGED
@@ -5,9 +5,7 @@ import {
5
  chatGenerationStatePubSub,
6
  chatInputPubSub,
7
  followUpQuestionPubSub,
8
- isRestoringFromHistoryPubSub,
9
- suppressNextFollowUpPubSub,
10
- } from "@/modules/pubSub";
11
 
12
  interface ChatInputAreaProps {
13
  onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
@@ -18,18 +16,13 @@ function ChatInputArea({ onKeyDown, handleSend }: ChatInputAreaProps) {
18
  const [input, setInput] = usePubSub(chatInputPubSub);
19
  const [generationState] = usePubSub(chatGenerationStatePubSub);
20
  const [followUpQuestion] = usePubSub(followUpQuestionPubSub);
21
- const [isRestoringFromHistory] = usePubSub(isRestoringFromHistoryPubSub);
22
- const [suppressNextFollowUp] = usePubSub(suppressNextFollowUpPubSub);
23
 
24
  const isGenerating =
25
  generationState.isGeneratingResponse &&
26
  !generationState.isGeneratingFollowUpQuestion;
27
 
28
- const defaultPlaceholder = "Anything else you would like to know?";
29
  const placeholder =
30
- isRestoringFromHistory || suppressNextFollowUp
31
- ? defaultPlaceholder
32
- : followUpQuestion || defaultPlaceholder;
33
 
34
  const onChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
35
  setInput(event.target.value);
@@ -37,12 +30,7 @@ function ChatInputArea({ onKeyDown, handleSend }: ChatInputAreaProps) {
37
  const handleKeyDownWithPlaceholder = (
38
  event: React.KeyboardEvent<HTMLTextAreaElement>,
39
  ) => {
40
- if (
41
- input.trim() === "" &&
42
- followUpQuestion &&
43
- !isRestoringFromHistory &&
44
- !suppressNextFollowUp
45
- ) {
46
  if (event.key === "Enter" && !event.shiftKey) {
47
  event.preventDefault();
48
  handleSend(followUpQuestion);
@@ -54,12 +42,7 @@ function ChatInputArea({ onKeyDown, handleSend }: ChatInputAreaProps) {
54
  };
55
 
56
  const handleSendWithPlaceholder = () => {
57
- if (
58
- input.trim() === "" &&
59
- followUpQuestion &&
60
- !isRestoringFromHistory &&
61
- !suppressNextFollowUp
62
- ) {
63
  handleSend(followUpQuestion);
64
  } else {
65
  handleSend();
@@ -77,7 +60,7 @@ function ChatInputArea({ onKeyDown, handleSend }: ChatInputAreaProps) {
77
  onKeyDown={handleKeyDownWithPlaceholder}
78
  autosize
79
  minRows={1}
80
- maxRows={8}
81
  style={{ flexGrow: 1, paddingRight: "50px" }}
82
  disabled={isGenerating}
83
  />
 
5
  chatGenerationStatePubSub,
6
  chatInputPubSub,
7
  followUpQuestionPubSub,
8
+ } from "../../modules/pubSub";
 
 
9
 
10
  interface ChatInputAreaProps {
11
  onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
 
16
  const [input, setInput] = usePubSub(chatInputPubSub);
17
  const [generationState] = usePubSub(chatGenerationStatePubSub);
18
  const [followUpQuestion] = usePubSub(followUpQuestionPubSub);
 
 
19
 
20
  const isGenerating =
21
  generationState.isGeneratingResponse &&
22
  !generationState.isGeneratingFollowUpQuestion;
23
 
 
24
  const placeholder =
25
+ followUpQuestion || "Anything else you would like to know?";
 
 
26
 
27
  const onChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
28
  setInput(event.target.value);
 
30
  const handleKeyDownWithPlaceholder = (
31
  event: React.KeyboardEvent<HTMLTextAreaElement>,
32
  ) => {
33
+ if (input.trim() === "" && followUpQuestion) {
 
 
 
 
 
34
  if (event.key === "Enter" && !event.shiftKey) {
35
  event.preventDefault();
36
  handleSend(followUpQuestion);
 
42
  };
43
 
44
  const handleSendWithPlaceholder = () => {
45
+ if (input.trim() === "" && followUpQuestion) {
 
 
 
 
 
46
  handleSend(followUpQuestion);
47
  } else {
48
  handleSend();
 
60
  onKeyDown={handleKeyDownWithPlaceholder}
61
  autosize
62
  minRows={1}
63
+ maxRows={4}
64
  style={{ flexGrow: 1, paddingRight: "50px" }}
65
  disabled={isGenerating}
66
  />
client/components/AiResponse/ChatInterface.tsx CHANGED
@@ -1,39 +1,25 @@
1
  import { Card, Stack } from "@mantine/core";
2
  import { usePubSub } from "create-pubsub/react";
3
- import {
4
- type KeyboardEvent,
5
- useCallback,
6
- useEffect,
7
- useRef,
8
- useState,
9
- } from "react";
10
  import throttle from "throttleit";
11
- import { generateFollowUpQuestion } from "@/modules/followUpQuestions";
12
- import {
13
- getCurrentSearchRunId,
14
- saveChatMessageForQuery,
15
- updateSearchResults,
16
- } from "@/modules/history";
17
- import { handleEnterKeyDown } from "@/modules/keyboard";
18
- import { addLogEntry } from "@/modules/logEntries";
19
  import {
20
  chatGenerationStatePubSub,
21
  chatInputPubSub,
22
  followUpQuestionPubSub,
23
- getSettings,
24
- imageSearchResultsPubSub,
25
- queryPubSub,
26
  settingsPubSub,
27
- suppressNextFollowUpPubSub,
28
- textSearchResultsPubSub,
29
  updateImageSearchResults,
30
  updateLlmTextSearchResults,
31
  updateTextSearchResults,
32
- } from "@/modules/pubSub";
33
- import { generateRelatedSearchQuery } from "@/modules/relatedSearchQuery";
34
- import { searchImages, searchText } from "@/modules/search";
35
- import { generateChatResponse } from "@/modules/textGeneration";
36
- import type { ChatMessage } from "@/modules/types";
37
  import ChatHeader from "./ChatHeader";
38
  import ChatInputArea from "./ChatInputArea";
39
  import MessageList from "./MessageList";
@@ -41,45 +27,23 @@ import MessageList from "./MessageList";
41
  interface ChatInterfaceProps {
42
  initialQuery?: string;
43
  initialResponse?: string;
44
- initialMessages?: ChatMessage[];
45
- suppressInitialFollowUp?: boolean;
46
  }
47
 
48
  export default function ChatInterface({
49
  initialQuery,
50
  initialResponse,
51
- initialMessages,
52
- suppressInitialFollowUp,
53
  }: ChatInterfaceProps) {
54
- const initialMessagesArray =
55
- initialMessages &&
56
- initialMessages.length > 0 &&
57
- initialQuery &&
58
- initialResponse
59
- ? [
60
- { role: "user" as const, content: initialQuery },
61
- { role: "assistant" as const, content: initialResponse },
62
- ...initialMessages,
63
- ]
64
- : initialMessages || [];
65
-
66
- const [messages, setMessages] = useState<ChatMessage[]>(initialMessagesArray);
67
  const [input, setInput] = usePubSub(chatInputPubSub);
68
  const [generationState, setGenerationState] = usePubSub(
69
  chatGenerationStatePubSub,
70
  );
71
  const [, setFollowUpQuestion] = usePubSub(followUpQuestionPubSub);
72
- const [textSearchResults] = usePubSub(textSearchResultsPubSub);
73
- const [imageSearchResults] = usePubSub(imageSearchResultsPubSub);
74
- const [currentQuery] = usePubSub(queryPubSub);
75
- const [suppressNextFollowUp] = usePubSub(suppressNextFollowUpPubSub);
76
  const [previousFollowUpQuestions, setPreviousFollowUpQuestions] = useState<
77
  string[]
78
  >([]);
79
  const [settings] = usePubSub(settingsPubSub);
80
  const [streamedResponse, setStreamedResponse] = useState("");
81
- const hasInitialized = useRef(false);
82
- const prevInitialMessagesRef = useRef<ChatMessage[] | undefined>(undefined);
83
  const updateStreamedResponse = useCallback(
84
  throttle((response: string) => {
85
  setStreamedResponse(response);
@@ -89,7 +53,6 @@ export default function ChatInterface({
89
 
90
  const regenerateFollowUpQuestion = useCallback(
91
  async (currentQuery: string, currentResponse: string) => {
92
- if (suppressNextFollowUp) return;
93
  if (!currentResponse || !currentQuery.trim()) return;
94
 
95
  try {
@@ -120,298 +83,138 @@ export default function ChatInterface({
120
  });
121
  }
122
  },
123
- [
124
- setFollowUpQuestion,
125
- setGenerationState,
126
- previousFollowUpQuestions,
127
- suppressNextFollowUp,
128
- ],
129
  );
130
 
131
  useEffect(() => {
132
- const messagesChanged =
133
- !prevInitialMessagesRef.current ||
134
- JSON.stringify(prevInitialMessagesRef.current) !==
135
- JSON.stringify(initialMessages);
136
-
137
- if (!messagesChanged) return;
138
-
139
- prevInitialMessagesRef.current = initialMessages;
140
-
141
- const newInitialMessagesArray =
142
- initialMessages &&
143
- initialMessages.length > 0 &&
144
- initialQuery &&
145
- initialResponse
146
- ? [
147
- { role: "user" as const, content: initialQuery },
148
- { role: "assistant" as const, content: initialResponse },
149
- ...initialMessages,
150
- ]
151
- : initialMessages || [];
152
-
153
- if (newInitialMessagesArray.length > 0) {
154
- setMessages(newInitialMessagesArray);
155
- } else if (initialQuery && initialResponse) {
156
  setMessages([
157
  { role: "user", content: initialQuery },
158
  { role: "assistant", content: initialResponse },
159
  ]);
160
- }
161
- }, [initialQuery, initialResponse, initialMessages]);
162
-
163
- useEffect(() => {
164
- if (suppressNextFollowUp) {
165
- hasInitialized.current = true;
166
- return;
167
- }
168
- if (suppressInitialFollowUp) return;
169
- if (hasInitialized.current) return;
170
-
171
- if (initialMessages && initialMessages.length > 0) {
172
- const lastAssistant = messages
173
- .filter((m) => m.role === "assistant")
174
- .pop();
175
- const lastUser = messages.filter((m) => m.role === "user").pop();
176
- if (lastUser && lastAssistant) {
177
- regenerateFollowUpQuestion(lastUser.content, lastAssistant.content);
178
- hasInitialized.current = true;
179
- }
180
- } else if (messages.length >= 2 && initialQuery && initialResponse) {
181
  regenerateFollowUpQuestion(initialQuery, initialResponse);
182
- hasInitialized.current = true;
183
  }
184
  }, [
185
  initialQuery,
186
  initialResponse,
187
- initialMessages,
188
- messages,
189
  regenerateFollowUpQuestion,
190
- suppressInitialFollowUp,
191
- suppressNextFollowUp,
192
  ]);
193
 
194
- useEffect(() => {
195
- return () => {
196
- setFollowUpQuestion("");
197
- setPreviousFollowUpQuestions([]);
198
- };
199
- }, [setFollowUpQuestion]);
200
-
201
- const handleEditMessage = useCallback(
202
- (absoluteIndex: number) => {
203
- const target = messages[absoluteIndex];
204
- if (!target || target.role !== "user") return;
205
- setInput(target.content);
206
- setMessages(messages.slice(0, absoluteIndex));
207
- setFollowUpQuestion("");
208
- },
209
- [messages, setInput, setFollowUpQuestion],
210
- );
211
-
212
- const handleRegenerateResponse = useCallback(async () => {
213
- if (
214
- generationState.isGeneratingResponse ||
215
- messages.length < 3 ||
216
- messages[messages.length - 1].role !== "assistant"
217
- )
218
  return;
219
 
220
- const history = messages.slice(0, -1);
221
- const lastUser = history[history.length - 1];
222
 
223
- setMessages(history);
224
- setGenerationState({ ...generationState, isGeneratingResponse: true });
 
 
 
 
 
 
 
 
225
  setFollowUpQuestion("");
226
  setStreamedResponse("");
227
 
228
  try {
229
- const finalResponse = await generateChatResponse(
230
- history,
231
- updateStreamedResponse,
232
- );
233
 
234
- setMessages((prev) => [
235
- ...prev,
236
- { role: "assistant", content: finalResponse },
237
- ]);
238
-
239
- addLogEntry("AI response re-generated");
240
-
241
- if (lastUser?.role === "user") {
242
- await regenerateFollowUpQuestion(lastUser.content, finalResponse);
243
- }
244
- } catch (error) {
245
- addLogEntry(`Error re-generating response: ${error}`);
246
- } finally {
247
- setGenerationState({ ...generationState, isGeneratingResponse: false });
248
- }
249
- }, [
250
- generationState,
251
- messages,
252
- regenerateFollowUpQuestion,
253
- setFollowUpQuestion,
254
- setGenerationState,
255
- updateStreamedResponse,
256
- ]);
257
-
258
- const handleSend = useCallback(
259
- async (textToSend?: string) => {
260
- const currentInput = textToSend ?? input;
261
- if (currentInput.trim() === "" || generationState.isGeneratingResponse)
262
- return;
263
-
264
- const userMessage: ChatMessage = { role: "user", content: currentInput };
265
- const newMessages: ChatMessage[] = [...messages, userMessage];
266
-
267
- setMessages(newMessages);
268
- if (!textToSend) setInput("");
269
- setGenerationState({
270
- ...generationState,
271
- isGeneratingResponse: true,
272
- });
273
- setFollowUpQuestion("");
274
- setStreamedResponse("");
275
-
276
- try {
277
- const relatedQuery = await generateRelatedSearchQuery([...newMessages]);
278
- const searchQuery = relatedQuery || currentInput;
279
 
280
- if (settings.enableTextSearch) {
281
- const freshResults = await searchText(
282
- searchQuery,
283
- settings.searchResultsLimit,
284
  );
285
 
286
- if (freshResults.length > 0) {
287
- const existingUrls = new Set(
288
- textSearchResults.map(([, , url]) => url),
289
- );
290
-
291
- const uniqueFreshResults = freshResults.filter(
292
- ([, , url]) => !existingUrls.has(url),
293
- );
294
 
 
 
 
 
 
295
  updateLlmTextSearchResults(
296
- freshResults.slice(0, getSettings().searchResultsToConsider),
297
  );
298
-
299
- if (uniqueFreshResults.length > 0) {
300
- const updatedResults = [
301
- ...textSearchResults,
302
- ...uniqueFreshResults,
303
- ];
304
- updateTextSearchResults(updatedResults);
305
-
306
- updateSearchResults(getCurrentSearchRunId(), {
307
- type: "text",
308
- items: updatedResults.map(([title, snippet, url]) => ({
309
- title,
310
- url,
311
- snippet,
312
- })),
313
- });
314
- }
315
  }
316
  }
 
317
 
318
- if (settings.enableImageSearch) {
319
- searchImages(searchQuery, settings.searchResultsLimit)
320
- .then((imageResults) => {
321
- if (imageResults.length > 0) {
322
- const existingUrls = new Set(
323
- imageSearchResults.map(([, url]) => url),
324
- );
325
-
326
- const uniqueFreshResults = imageResults.filter(
327
- ([, url]) => !existingUrls.has(url),
328
- );
329
-
330
- if (uniqueFreshResults.length > 0) {
331
- const updatedImageResults = [
332
- ...uniqueFreshResults,
333
- ...imageSearchResults,
334
- ];
335
- updateImageSearchResults(updatedImageResults);
336
-
337
- updateSearchResults(getCurrentSearchRunId(), {
338
- type: "image",
339
- items: updatedImageResults.map(
340
- ([title, url, thumbnailUrl, sourceUrl]) => ({
341
- title,
342
- url,
343
- thumbnail: thumbnailUrl,
344
- sourceUrl,
345
- }),
346
- ),
347
- });
348
- }
349
  }
350
- })
351
- .catch((error) => {
352
- addLogEntry(`Error in follow-up image search: ${error}`);
353
- });
354
- }
355
- } catch (error) {
356
- addLogEntry(`Error in follow-up search: ${error}`);
357
  }
 
 
 
358
 
359
- try {
360
- const finalResponse = await generateChatResponse(
361
- newMessages,
362
- updateStreamedResponse,
363
- );
364
-
365
- setMessages((prevMessages) => [
366
- ...prevMessages,
367
- { role: "assistant", content: finalResponse },
368
- ]);
369
 
370
- addLogEntry("AI response completed");
 
 
 
371
 
372
- await saveChatMessageForQuery(currentQuery, "user", currentInput);
373
- await saveChatMessageForQuery(currentQuery, "assistant", finalResponse);
374
 
375
- await regenerateFollowUpQuestion(currentInput, finalResponse);
376
- } catch (error) {
377
- addLogEntry(`Error in chat response: ${error}`);
378
- setMessages((prevMessages) => [
379
- ...prevMessages,
380
- {
381
- role: "assistant",
382
- content:
383
- "Sorry, I encountered an error while generating a response.",
384
- },
385
- ]);
386
- } finally {
387
- setGenerationState({
388
- ...generationState,
389
- isGeneratingResponse: false,
390
- });
391
- }
392
- },
393
- [
394
- generationState,
395
- messages,
396
- settings,
397
- input,
398
- regenerateFollowUpQuestion,
399
- setFollowUpQuestion,
400
- setGenerationState,
401
- setInput,
402
- updateStreamedResponse,
403
- currentQuery,
404
- textSearchResults,
405
- imageSearchResults,
406
- ],
407
- );
408
 
409
- const handleKeyDown = useCallback(
410
- (event: KeyboardEvent<HTMLTextAreaElement>) => {
411
- handleEnterKeyDown(event, settings, handleSend);
412
- },
413
- [settings, handleSend],
414
- );
415
 
416
  return (
417
  <Card withBorder shadow="sm" radius="md">
@@ -425,9 +228,6 @@ export default function ChatInterface({
425
  ? [...messages, { role: "assistant", content: streamedResponse }]
426
  : messages
427
  }
428
- onEditMessage={handleEditMessage}
429
- onRegenerate={handleRegenerateResponse}
430
- isGenerating={generationState.isGeneratingResponse}
431
  />
432
  <ChatInputArea onKeyDown={handleKeyDown} handleSend={handleSend} />
433
  </Stack>
 
1
  import { Card, Stack } from "@mantine/core";
2
  import { usePubSub } from "create-pubsub/react";
3
+ import type { ChatMessage } from "gpt-tokenizer/GptEncoding";
4
+ import { type KeyboardEvent, useCallback, useEffect, useState } from "react";
 
 
 
 
 
5
  import throttle from "throttleit";
6
+ import { generateFollowUpQuestion } from "../../modules/followUpQuestions";
7
+ import { handleEnterKeyDown } from "../../modules/keyboard";
8
+ import { addLogEntry } from "../../modules/logEntries";
 
 
 
 
 
9
  import {
10
  chatGenerationStatePubSub,
11
  chatInputPubSub,
12
  followUpQuestionPubSub,
13
+ getImageSearchResults,
14
+ getTextSearchResults,
 
15
  settingsPubSub,
 
 
16
  updateImageSearchResults,
17
  updateLlmTextSearchResults,
18
  updateTextSearchResults,
19
+ } from "../../modules/pubSub";
20
+ import { generateRelatedSearchQuery } from "../../modules/relatedSearchQuery";
21
+ import { searchImages, searchText } from "../../modules/search";
22
+ import { generateChatResponse } from "../../modules/textGeneration";
 
23
  import ChatHeader from "./ChatHeader";
24
  import ChatInputArea from "./ChatInputArea";
25
  import MessageList from "./MessageList";
 
27
  interface ChatInterfaceProps {
28
  initialQuery?: string;
29
  initialResponse?: string;
 
 
30
  }
31
 
32
  export default function ChatInterface({
33
  initialQuery,
34
  initialResponse,
 
 
35
  }: ChatInterfaceProps) {
36
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
 
 
 
 
 
 
 
 
 
 
 
 
37
  const [input, setInput] = usePubSub(chatInputPubSub);
38
  const [generationState, setGenerationState] = usePubSub(
39
  chatGenerationStatePubSub,
40
  );
41
  const [, setFollowUpQuestion] = usePubSub(followUpQuestionPubSub);
 
 
 
 
42
  const [previousFollowUpQuestions, setPreviousFollowUpQuestions] = useState<
43
  string[]
44
  >([]);
45
  const [settings] = usePubSub(settingsPubSub);
46
  const [streamedResponse, setStreamedResponse] = useState("");
 
 
47
  const updateStreamedResponse = useCallback(
48
  throttle((response: string) => {
49
  setStreamedResponse(response);
 
53
 
54
  const regenerateFollowUpQuestion = useCallback(
55
  async (currentQuery: string, currentResponse: string) => {
 
56
  if (!currentResponse || !currentQuery.trim()) return;
57
 
58
  try {
 
83
  });
84
  }
85
  },
86
+ [setFollowUpQuestion, setGenerationState, previousFollowUpQuestions],
 
 
 
 
 
87
  );
88
 
89
  useEffect(() => {
90
+ if (messages.length === 0 && initialQuery && initialResponse) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  setMessages([
92
  { role: "user", content: initialQuery },
93
  { role: "assistant", content: initialResponse },
94
  ]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  regenerateFollowUpQuestion(initialQuery, initialResponse);
 
96
  }
97
  }, [
98
  initialQuery,
99
  initialResponse,
100
+ messages.length,
 
101
  regenerateFollowUpQuestion,
 
 
102
  ]);
103
 
104
+ const handleSend = async (textToSend?: string) => {
105
+ const currentInput = textToSend ?? input;
106
+ if (currentInput.trim() === "" || generationState.isGeneratingResponse)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  return;
108
 
109
+ const userMessage: ChatMessage = { role: "user", content: currentInput };
110
+ const newMessages: ChatMessage[] = [...messages, userMessage];
111
 
112
+ if (messages.length === 0) {
113
+ setPreviousFollowUpQuestions([]);
114
+ }
115
+
116
+ setMessages(newMessages);
117
+ setInput(textToSend ? input : "");
118
+ setGenerationState({
119
+ ...generationState,
120
+ isGeneratingResponse: true,
121
+ });
122
  setFollowUpQuestion("");
123
  setStreamedResponse("");
124
 
125
  try {
126
+ const relatedQuery = await generateRelatedSearchQuery([...newMessages]);
127
+ const searchQuery = relatedQuery || currentInput;
 
 
128
 
129
+ if (settings.enableTextSearch) {
130
+ const freshResults = await searchText(
131
+ searchQuery,
132
+ settings.searchResultsLimit,
133
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
+ if (freshResults.length > 0) {
136
+ const existingUrls = new Set(
137
+ getTextSearchResults().map(([, , url]) => url),
 
138
  );
139
 
140
+ const uniqueFreshResults = freshResults.filter(
141
+ ([, , url]) => !existingUrls.has(url),
142
+ );
 
 
 
 
 
143
 
144
+ if (uniqueFreshResults.length > 0) {
145
+ updateTextSearchResults([
146
+ ...getTextSearchResults(),
147
+ ...uniqueFreshResults,
148
+ ]);
149
  updateLlmTextSearchResults(
150
+ uniqueFreshResults.slice(0, settings.searchResultsToConsider),
151
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  }
153
  }
154
+ }
155
 
156
+ if (settings.enableImageSearch) {
157
+ searchImages(searchQuery, settings.searchResultsLimit)
158
+ .then((imageResults) => {
159
+ if (imageResults.length > 0) {
160
+ const existingUrls = new Set(
161
+ getImageSearchResults().map(([, url]) => url),
162
+ );
163
+
164
+ const uniqueFreshResults = imageResults.filter(
165
+ ([, url]) => !existingUrls.has(url),
166
+ );
167
+
168
+ if (uniqueFreshResults.length > 0) {
169
+ updateImageSearchResults([
170
+ ...uniqueFreshResults,
171
+ ...getImageSearchResults(),
172
+ ]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  }
174
+ }
175
+ })
176
+ .catch((error) => {
177
+ addLogEntry(`Error in follow-up image search: ${error}`);
178
+ });
 
 
179
  }
180
+ } catch (error) {
181
+ addLogEntry(`Error in follow-up search: ${error}`);
182
+ }
183
 
184
+ try {
185
+ const finalResponse = await generateChatResponse(
186
+ newMessages,
187
+ updateStreamedResponse,
188
+ );
 
 
 
 
 
189
 
190
+ setMessages((prevMessages) => [
191
+ ...prevMessages,
192
+ { role: "assistant", content: finalResponse },
193
+ ]);
194
 
195
+ addLogEntry("AI response completed");
 
196
 
197
+ await regenerateFollowUpQuestion(currentInput, finalResponse);
198
+ } catch (error) {
199
+ addLogEntry(`Error in chat response: ${error}`);
200
+ setMessages((prevMessages) => [
201
+ ...prevMessages,
202
+ {
203
+ role: "assistant",
204
+ content: "Sorry, I encountered an error while generating a response.",
205
+ },
206
+ ]);
207
+ } finally {
208
+ setGenerationState({
209
+ ...generationState,
210
+ isGeneratingResponse: false,
211
+ });
212
+ }
213
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
+ const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
216
+ handleEnterKeyDown(event, settings, handleSend);
217
+ };
 
 
 
218
 
219
  return (
220
  <Card withBorder shadow="sm" radius="md">
 
228
  ? [...messages, { role: "assistant", content: streamedResponse }]
229
  : messages
230
  }
 
 
 
231
  />
232
  <ChatInputArea onKeyDown={handleKeyDown} handleSend={handleSend} />
233
  </Stack>
client/components/AiResponse/EnableAiResponsePrompt.tsx CHANGED
@@ -2,7 +2,7 @@ import {
2
  ActionIcon,
3
  Alert,
4
  Button,
5
- Grid,
6
  Group,
7
  Popover,
8
  Stack,
@@ -39,47 +39,42 @@ export default function EnableAiResponsePrompt({
39
 
40
  return (
41
  <Alert variant="light" color="blue" p="xs">
42
- <Grid justify="space-between" align="center">
43
- <Grid.Col span="content">
44
- <Group gap="xs">
45
- <Text fw={500}>Enable AI Responses?</Text>
46
- <Popover
47
- width={300}
48
- styles={{ dropdown: { maxWidth: "92vw" } }}
49
- position="bottom"
50
- withArrow
51
- shadow="md"
52
- >
53
- <Popover.Target>
54
- <ActionIcon variant="subtle" color="blue" size="sm">
55
- <IconInfoCircle size="1rem" />
56
- </ActionIcon>
57
- </Popover.Target>
58
- <Popover.Dropdown>{helpContent}</Popover.Dropdown>
59
- </Popover>
60
- </Group>
61
- </Grid.Col>
62
- <Grid.Col span="auto">
63
- <Group justify="end">
64
- <Button
65
- variant="subtle"
66
- color="gray"
67
- leftSection={<IconX size="1rem" />}
68
- onClick={onDecline}
69
- size="xs"
70
- >
71
- No, thanks
72
- </Button>
73
- <Button
74
- leftSection={<IconCheck size="1rem" />}
75
- onClick={onAccept}
76
- size="xs"
77
- >
78
- Yes, please
79
- </Button>
80
- </Group>
81
- </Grid.Col>
82
- </Grid>
83
  </Alert>
84
  );
85
  }
 
2
  ActionIcon,
3
  Alert,
4
  Button,
5
+ Flex,
6
  Group,
7
  Popover,
8
  Stack,
 
39
 
40
  return (
41
  <Alert variant="light" color="blue" p="xs">
42
+ <Flex align="center" gap="xs">
43
+ <Text fw={500}>Enable AI Responses?</Text>
44
+ <Popover
45
+ width={300}
46
+ styles={{ dropdown: { maxWidth: "92vw" } }}
47
+ position="bottom"
48
+ withArrow
49
+ shadow="md"
50
+ >
51
+ <Popover.Target>
52
+ <ActionIcon variant="subtle" color="blue" size="sm">
53
+ <IconInfoCircle size="1rem" />
54
+ </ActionIcon>
55
+ </Popover.Target>
56
+ <Popover.Dropdown>{helpContent}</Popover.Dropdown>
57
+ </Popover>
58
+ <div style={{ flex: 1 }} />
59
+ <Group>
60
+ <Button
61
+ variant="subtle"
62
+ color="gray"
63
+ leftSection={<IconX size="1rem" />}
64
+ onClick={onDecline}
65
+ size="xs"
66
+ >
67
+ No, thanks
68
+ </Button>
69
+ <Button
70
+ leftSection={<IconCheck size="1rem" />}
71
+ onClick={onAccept}
72
+ size="xs"
73
+ >
74
+ Yes, please
75
+ </Button>
76
+ </Group>
77
+ </Flex>
 
 
 
 
 
78
  </Alert>
79
  );
80
  }
client/components/AiResponse/FormattedMarkdown.tsx CHANGED
@@ -17,7 +17,7 @@ export default function FormattedMarkdown({
17
  const { reasoningContent, mainContent, isGenerating } =
18
  useReasoningContent(children);
19
 
20
- if (!children && !reasoningContent) {
21
  return null;
22
  }
23
 
 
17
  const { reasoningContent, mainContent, isGenerating } =
18
  useReasoningContent(children);
19
 
20
+ if (!children) {
21
  return null;
22
  }
23
 
client/components/AiResponse/MarkdownRenderer.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import { CodeHighlight } from "@mantine/code-highlight";
2
- import { Blockquote, Box, Code, Divider, Text } from "@mantine/core";
3
  import React from "react";
4
  import { ErrorBoundary } from "react-error-boundary";
5
  import Markdown from "react-markdown";
@@ -22,15 +22,6 @@ export default function MarkdownRenderer({
22
  return null;
23
  }
24
 
25
- const unwrapParagraphs = (children: React.ReactNode) => {
26
- return React.Children.map(children, (child) => {
27
- if (React.isValidElement(child) && child.type === "p") {
28
- return (child.props as { children: React.ReactNode }).children;
29
- }
30
- return child;
31
- });
32
- };
33
-
34
  return (
35
  <Box className={className}>
36
  <Markdown
@@ -50,7 +41,13 @@ export default function MarkdownRenderer({
50
  },
51
  li(props) {
52
  const { children } = props;
53
- return <li>{unwrapParagraphs(children)}</li>;
 
 
 
 
 
 
54
  },
55
  hr() {
56
  return <Divider variant="dashed" my="md" />;
@@ -58,14 +55,6 @@ export default function MarkdownRenderer({
58
  pre(props) {
59
  return <>{props.children}</>;
60
  },
61
- blockquote(props) {
62
- const { children } = props;
63
- return (
64
- <Blockquote>
65
- <Text size="md">{unwrapParagraphs(children)}</Text>
66
- </Blockquote>
67
- );
68
- },
69
  code(props) {
70
  const { children, className, node } = props;
71
  const codeContent = children?.toString().replace(/\n$/, "") ?? "";
 
1
  import { CodeHighlight } from "@mantine/code-highlight";
2
+ import { Box, Code, Divider } from "@mantine/core";
3
  import React from "react";
4
  import { ErrorBoundary } from "react-error-boundary";
5
  import Markdown from "react-markdown";
 
22
  return null;
23
  }
24
 
 
 
 
 
 
 
 
 
 
25
  return (
26
  <Box className={className}>
27
  <Markdown
 
41
  },
42
  li(props) {
43
  const { children } = props;
44
+ const processedChildren = React.Children.map(children, (child) => {
45
+ if (React.isValidElement(child) && child.type === "p") {
46
+ return (child.props as { children: React.ReactNode }).children;
47
+ }
48
+ return child;
49
+ });
50
+ return <li>{processedChildren}</li>;
51
  },
52
  hr() {
53
  return <Divider variant="dashed" my="md" />;
 
55
  pre(props) {
56
  return <>{props.children}</>;
57
  },
 
 
 
 
 
 
 
 
58
  code(props) {
59
  const { children, className, node } = props;
60
  const codeContent = children?.toString().replace(/\n$/, "") ?? "";
client/components/AiResponse/MessageList.tsx CHANGED
@@ -1,101 +1,43 @@
1
- import { ActionIcon, Group, Paper, Stack, Tooltip } from "@mantine/core";
2
- import { IconPencil, IconRefresh } from "@tabler/icons-react";
3
  import { memo } from "react";
4
- import type { ChatMessage } from "@/modules/types";
5
  import FormattedMarkdown from "./FormattedMarkdown";
6
 
7
  interface MessageListProps {
8
  messages: ChatMessage[];
9
- onEditMessage: (absoluteIndex: number) => void;
10
- onRegenerate: () => void;
11
- isGenerating: boolean;
12
  }
13
 
14
  interface MessageProps {
15
  message: ChatMessage;
16
  index: number;
17
- absoluteIndex: number;
18
- isLastAssistant: boolean;
19
- isGenerating: boolean;
20
- onEditMessage: (absoluteIndex: number) => void;
21
- onRegenerate: () => void;
22
  }
23
 
24
  const Message = memo(
25
- ({
26
- message,
27
- index,
28
- absoluteIndex,
29
- isLastAssistant,
30
- isGenerating,
31
- onEditMessage,
32
- onRegenerate,
33
- }: MessageProps) => {
34
- const canEdit = message.role === "user";
35
- const canRegenerate = isLastAssistant && message.role === "assistant";
36
- const iconSize = 16;
37
- const iconVariant: "subtle" = "subtle";
38
-
39
  return (
40
- <Group
41
- gap="xs"
42
- align="center"
43
- w="100%"
44
- justify={message.role === "user" ? "flex-end" : "flex-start"}
 
 
 
 
45
  >
46
- {canEdit && (
47
- <Tooltip label="Edit" withArrow position="right" openDelay={300}>
48
- <ActionIcon
49
- aria-label="Edit message"
50
- color="gray"
51
- variant={iconVariant}
52
- disabled={isGenerating}
53
- onClick={() => onEditMessage(absoluteIndex)}
54
- >
55
- <IconPencil size={iconSize} />
56
- </ActionIcon>
57
- </Tooltip>
58
- )}
59
-
60
- <Paper
61
- key={`${message.role}-${index}`}
62
- shadow="xs"
63
- radius="xl"
64
- p="sm"
65
- style={{ flex: 1, overflow: "auto" }}
66
- >
67
- <FormattedMarkdown>{message.content}</FormattedMarkdown>
68
- </Paper>
69
-
70
- {canRegenerate && (
71
- <Tooltip
72
- label="Re-generate response"
73
- withArrow
74
- position="left"
75
- openDelay={300}
76
- >
77
- <ActionIcon
78
- aria-label="Re-generate response"
79
- color="gray"
80
- variant={iconVariant}
81
- disabled={isGenerating}
82
- onClick={() => onRegenerate()}
83
- >
84
- <IconRefresh size={iconSize} />
85
- </ActionIcon>
86
- </Tooltip>
87
- )}
88
- </Group>
89
  );
90
  },
91
  );
92
 
93
- const MessageList = memo(function MessageList({
94
- messages,
95
- onEditMessage,
96
- onRegenerate,
97
- isGenerating,
98
- }: MessageListProps) {
99
  if (messages.length <= 2) return null;
100
 
101
  return (
@@ -103,24 +45,13 @@ const MessageList = memo(function MessageList({
103
  {messages
104
  .slice(2)
105
  .filter((message) => message.content.length > 0)
106
- .map((message, index) => {
107
- const absoluteIndex = index + 2;
108
- const isLastAssistant =
109
- absoluteIndex === messages.length - 1 &&
110
- message.role === "assistant";
111
- return (
112
- <Message
113
- key={`${message.role}-${message.content.slice(0, 50)}`}
114
- message={message}
115
- index={index}
116
- absoluteIndex={absoluteIndex}
117
- isLastAssistant={isLastAssistant}
118
- isGenerating={isGenerating}
119
- onEditMessage={onEditMessage}
120
- onRegenerate={onRegenerate}
121
- />
122
- );
123
- })}
124
  </Stack>
125
  );
126
  });
 
1
+ import { Paper, Stack } from "@mantine/core";
2
+ import type { ChatMessage } from "gpt-tokenizer/GptEncoding";
3
  import { memo } from "react";
 
4
  import FormattedMarkdown from "./FormattedMarkdown";
5
 
6
  interface MessageListProps {
7
  messages: ChatMessage[];
 
 
 
8
  }
9
 
10
  interface MessageProps {
11
  message: ChatMessage;
12
  index: number;
 
 
 
 
 
13
  }
14
 
15
  const Message = memo(
16
+ function Message({ message, index }: MessageProps) {
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  return (
18
+ <Paper
19
+ key={`${message.role}-${index}`}
20
+ shadow="xs"
21
+ radius="xl"
22
+ p="sm"
23
+ maw="90%"
24
+ style={{
25
+ alignSelf: message.role === "user" ? "flex-end" : "flex-start",
26
+ }}
27
  >
28
+ <FormattedMarkdown>{message.content}</FormattedMarkdown>
29
+ </Paper>
30
+ );
31
+ },
32
+ (prevProps, nextProps) => {
33
+ return (
34
+ prevProps.message.content === nextProps.message.content &&
35
+ prevProps.message.role === nextProps.message.role
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  );
37
  },
38
  );
39
 
40
+ const MessageList = memo(function MessageList({ messages }: MessageListProps) {
 
 
 
 
 
41
  if (messages.length <= 2) return null;
42
 
43
  return (
 
45
  {messages
46
  .slice(2)
47
  .filter((message) => message.content.length > 0)
48
+ .map((message, index) => (
49
+ <Message
50
+ key={`${message.role}-${index}`}
51
+ message={message}
52
+ index={index}
53
+ />
54
+ ))}
 
 
 
 
 
 
 
 
 
 
 
55
  </Stack>
56
  );
57
  });
client/components/AiResponse/ReasoningSection.tsx CHANGED
@@ -8,7 +8,7 @@ import {
8
  UnstyledButton,
9
  } from "@mantine/core";
10
  import { IconChevronDown, IconChevronRight } from "@tabler/icons-react";
11
- import { useEffect, useState } from "react";
12
  import MarkdownRenderer from "./MarkdownRenderer";
13
 
14
  interface ReasoningSectionProps {
@@ -20,15 +20,7 @@ export default function ReasoningSection({
20
  content,
21
  isGenerating = false,
22
  }: ReasoningSectionProps) {
23
- const [isOpen, setIsOpen] = useState(isGenerating);
24
-
25
- useEffect(() => {
26
- if (isGenerating) {
27
- setIsOpen(true);
28
- } else {
29
- setIsOpen(false);
30
- }
31
- }, [isGenerating]);
32
 
33
  return (
34
  <Box mb="xs">
 
8
  UnstyledButton,
9
  } from "@mantine/core";
10
  import { IconChevronDown, IconChevronRight } from "@tabler/icons-react";
11
+ import { useState } from "react";
12
  import MarkdownRenderer from "./MarkdownRenderer";
13
 
14
  interface ReasoningSectionProps {
 
20
  content,
21
  isGenerating = false,
22
  }: ReasoningSectionProps) {
23
+ const [isOpen, setIsOpen] = useState(false);
 
 
 
 
 
 
 
 
24
 
25
  return (
26
  <Box mb="xs">
client/components/AiResponse/WebLlmModelSelect.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import { type ComboboxItem, Select } from "@mantine/core";
2
  import { prebuiltAppConfig } from "@mlc-ai/web-llm";
3
  import { useCallback, useEffect, useState } from "react";
4
- import { isF16Supported } from "@/modules/webGpu";
5
 
6
  export default function WebLlmModelSelect({
7
  value,
 
1
  import { type ComboboxItem, Select } from "@mantine/core";
2
  import { prebuiltAppConfig } from "@mlc-ai/web-llm";
3
  import { useCallback, useEffect, useState } from "react";
4
+ import { isF16Supported } from "../../modules/webGpu";
5
 
6
  export default function WebLlmModelSelect({
7
  value,
client/components/AiResponse/WllamaModelSelect.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import { type ComboboxItem, Select } from "@mantine/core";
2
  import { useEffect, useState } from "react";
3
- import { wllamaModels } from "@/modules/wllama";
4
 
5
  export default function WllamaModelSelect({
6
  value,
 
1
  import { type ComboboxItem, Select } from "@mantine/core";
2
  import { useEffect, useState } from "react";
3
+ import { wllamaModels } from "../../modules/wllama";
4
 
5
  export default function WllamaModelSelect({
6
  value,
client/components/AiResponse/hooks/useReasoningContent.test.ts DELETED
@@ -1,96 +0,0 @@
1
- import { renderHook } from "@testing-library/react";
2
- import { describe, expect, it } from "vitest";
3
- import { useReasoningContent } from "./useReasoningContent";
4
-
5
- const expectEmptyState = (result: {
6
- current: ReturnType<typeof useReasoningContent>;
7
- }) => {
8
- expect(result.current.reasoningContent).toBe("");
9
- expect(result.current.mainContent).toBe("");
10
- expect(result.current.isGenerating).toBe(false);
11
- };
12
-
13
- describe("useReasoningContent hook", () => {
14
- describe("parsing reasoning content from markdown markers", () => {
15
- it("should extract reasoning content between start and end markers", () => {
16
- const { result } = renderHook(() =>
17
- useReasoningContent(
18
- "<think>Let me think about this</think>\nHere is the answer.",
19
- ),
20
- );
21
-
22
- expect(result.current.reasoningContent).toBe("Let me think about this");
23
- expect(result.current.mainContent).toBe("\nHere is the answer.");
24
- expect(result.current.isGenerating).toBe(false);
25
- });
26
-
27
- it("should handle empty text", () => {
28
- const { result } = renderHook(() => useReasoningContent(""));
29
- expectEmptyState(result);
30
- });
31
-
32
- it("should return text as main content when no markers present", () => {
33
- const { result } = renderHook(() =>
34
- useReasoningContent("This is a normal response."),
35
- );
36
-
37
- expect(result.current.reasoningContent).toBe("");
38
- expect(result.current.mainContent).toBe("This is a normal response.");
39
- expect(result.current.isGenerating).toBe(false);
40
- });
41
-
42
- it("should detect generating state when end marker is missing", () => {
43
- const { result } = renderHook(() =>
44
- useReasoningContent("<think>I'm still thinking"),
45
- );
46
-
47
- expect(result.current.reasoningContent).toBe("I'm still thinking");
48
- expect(result.current.mainContent).toBe("");
49
- expect(result.current.isGenerating).toBe(true);
50
- });
51
-
52
- it("should handle whitespace-only content", () => {
53
- const { result } = renderHook(() => useReasoningContent(" "));
54
- expectEmptyState(result);
55
- });
56
-
57
- it("should handle null/undefined content gracefully", () => {
58
- const { result } = renderHook(() =>
59
- useReasoningContent(null as unknown as string),
60
- );
61
- expectEmptyState(result);
62
- });
63
- });
64
-
65
- describe("UI state management for reasoning section", () => {
66
- it("should provide correct isGenerating state for accordion title", () => {
67
- const streamingState = renderHook(() =>
68
- useReasoningContent("<think>Currently thinking..."),
69
- );
70
-
71
- expect(streamingState.result.current.isGenerating).toBe(true);
72
-
73
- const completedState = renderHook(() =>
74
- useReasoningContent(
75
- "<think>Thought process completed</think>\nHere is the answer.",
76
- ),
77
- );
78
-
79
- expect(completedState.result.current.isGenerating).toBe(false);
80
- });
81
-
82
- it("should handle transition from streaming to completed state", () => {
83
- const initial = renderHook(() =>
84
- useReasoningContent("<think>Building response..."),
85
- );
86
- expect(initial.result.current.isGenerating).toBe(true);
87
-
88
- const transitioned = renderHook(() =>
89
- useReasoningContent(
90
- "<think>Response built.</think>\nFinal answer here.",
91
- ),
92
- );
93
- expect(transitioned.result.current.isGenerating).toBe(false);
94
- });
95
- });
96
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
client/components/AiResponse/hooks/useReasoningContent.ts CHANGED
@@ -1,12 +1,7 @@
1
  import { usePubSub } from "create-pubsub/react";
2
  import { useCallback } from "react";
3
- import { settingsPubSub } from "@/modules/pubSub";
4
 
5
- /**
6
- * Hook for extracting reasoning content from AI responses
7
- * @param text - The full text response from the AI
8
- * @returns Object containing separated reasoning and main content
9
- */
10
  export function useReasoningContent(text: string) {
11
  const [settings] = usePubSub(settingsPubSub);
12
 
@@ -15,50 +10,28 @@ export function useReasoningContent(text: string) {
15
  if (!text)
16
  return { reasoningContent: "", mainContent: "", isGenerating: false };
17
 
18
- const trimmedText = text.trim();
19
-
20
- if (!trimmedText.startsWith(startMarker))
21
  return { reasoningContent: "", mainContent: text, isGenerating: false };
22
 
23
- const startIndex = trimmedText.indexOf(startMarker);
24
- const endIndex = trimmedText.indexOf(endMarker);
25
 
26
  if (endIndex === -1) {
27
  return {
28
- reasoningContent: trimmedText.slice(startIndex + startMarker.length),
29
  mainContent: "",
30
  isGenerating: true,
31
  };
32
  }
33
 
34
  return {
35
- reasoningContent: trimmedText.slice(
36
- startIndex + startMarker.length,
37
- endIndex,
38
- ),
39
- mainContent: trimmedText.slice(endIndex + endMarker.length),
40
  isGenerating: false,
41
  };
42
  },
43
  [],
44
  );
45
 
46
- if (text && text.trim() === "") {
47
- return {
48
- reasoningContent: "",
49
- mainContent: "",
50
- isGenerating: false,
51
- };
52
- }
53
-
54
- if (!text) {
55
- return {
56
- reasoningContent: "",
57
- mainContent: "",
58
- isGenerating: false,
59
- };
60
- }
61
-
62
  const result = extractReasoningAndMainContent(
63
  text,
64
  settings.reasoningStartMarker,
 
1
  import { usePubSub } from "create-pubsub/react";
2
  import { useCallback } from "react";
3
+ import { settingsPubSub } from "../../../modules/pubSub";
4
 
 
 
 
 
 
5
  export function useReasoningContent(text: string) {
6
  const [settings] = usePubSub(settingsPubSub);
7
 
 
10
  if (!text)
11
  return { reasoningContent: "", mainContent: "", isGenerating: false };
12
 
13
+ if (!text.trim().startsWith(startMarker))
 
 
14
  return { reasoningContent: "", mainContent: text, isGenerating: false };
15
 
16
+ const endIndex = text.indexOf(endMarker);
 
17
 
18
  if (endIndex === -1) {
19
  return {
20
+ reasoningContent: text.slice(startMarker.length),
21
  mainContent: "",
22
  isGenerating: true,
23
  };
24
  }
25
 
26
  return {
27
+ reasoningContent: text.slice(startMarker.length, endIndex),
28
+ mainContent: text.slice(endIndex + endMarker.length),
 
 
 
29
  isGenerating: false,
30
  };
31
  },
32
  [],
33
  );
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  const result = extractReasoningAndMainContent(
36
  text,
37
  settings.reasoningStartMarker,
client/components/Analytics/SearchStats.tsx DELETED
@@ -1,313 +0,0 @@
1
- import {
2
- Badge,
3
- Card,
4
- Center,
5
- Group,
6
- Progress,
7
- SimpleGrid,
8
- Stack,
9
- Text,
10
- ThemeIcon,
11
- Title,
12
- } from "@mantine/core";
13
- import { IconSearch } from "@tabler/icons-react";
14
- import { useMemo } from "react";
15
- import { useSearchHistory } from "@/hooks/useSearchHistory";
16
- import type { SearchEntry } from "@/modules/history";
17
- import { formatRelativeTime } from "@/modules/stringFormatters";
18
-
19
- interface SearchStatsProps {
20
- period?: "today" | "week" | "month" | "all";
21
- compact?: boolean;
22
- }
23
-
24
- interface StatsData {
25
- totalSearches: number;
26
- avgPerDay: number;
27
- mostActiveHour: number;
28
- topSources: { source: string; count: number; percentage: number }[];
29
- recentActivity: SearchEntry[];
30
- searchTrends: { date: string; count: number }[];
31
- }
32
-
33
- export default function SearchStats({
34
- period = "week",
35
- compact = false,
36
- }: SearchStatsProps) {
37
- const { recentSearches, isLoading } = useSearchHistory({ limit: 1000 });
38
-
39
- const stats = useMemo((): StatsData => {
40
- if (!recentSearches.length) {
41
- return {
42
- totalSearches: 0,
43
- avgPerDay: 0,
44
- mostActiveHour: 0,
45
- topSources: [],
46
- recentActivity: [],
47
- searchTrends: [],
48
- };
49
- }
50
-
51
- const now = new Date();
52
- const filterDate = new Date();
53
-
54
- switch (period) {
55
- case "today":
56
- filterDate.setHours(0, 0, 0, 0);
57
- break;
58
- case "week":
59
- filterDate.setDate(now.getDate() - 7);
60
- break;
61
- case "month":
62
- filterDate.setDate(now.getDate() - 30);
63
- break;
64
- default:
65
- filterDate.setFullYear(2000);
66
- }
67
-
68
- const filteredSearches = recentSearches.filter(
69
- (search) => search.timestamp >= filterDate.getTime(),
70
- );
71
-
72
- const totalSearches = filteredSearches.length;
73
-
74
- const dateCounts = new Map<string, number>();
75
- filteredSearches.forEach((search) => {
76
- const date = new Date(search.timestamp).toISOString().split("T")[0];
77
- dateCounts.set(date, (dateCounts.get(date) || 0) + 1);
78
- });
79
-
80
- const uniqueDays = dateCounts.size;
81
- const avgPerDay =
82
- uniqueDays > 0 ? Math.round(totalSearches / uniqueDays) : 0;
83
-
84
- const hourCounts = new Array(24).fill(0);
85
- filteredSearches.forEach((search) => {
86
- const hour = new Date(search.timestamp).getHours();
87
- hourCounts[hour]++;
88
- });
89
- const mostActiveHour = hourCounts.indexOf(Math.max(...hourCounts));
90
-
91
- const sourceCounts = filteredSearches.reduce(
92
- (acc, search) => {
93
- if (compact && search.source?.toLowerCase() === "user") {
94
- return acc;
95
- }
96
- const source = search.source || "unknown";
97
- acc[source] = (acc[source] || 0) + 1;
98
- return acc;
99
- },
100
- {} as Record<string, number>,
101
- );
102
-
103
- const sourcesTotal = Object.values(sourceCounts).reduce(
104
- (sum, n) => sum + n,
105
- 0,
106
- );
107
- const topSources = Object.entries(sourceCounts)
108
- .map(([source, count]) => ({
109
- source: source.charAt(0).toUpperCase() + source.slice(1),
110
- count,
111
- percentage:
112
- sourcesTotal > 0 ? Math.round((count / sourcesTotal) * 100) : 0,
113
- }))
114
- .sort((a, b) => b.count - a.count);
115
-
116
- const recentActivity = filteredSearches
117
- .sort((a, b) => b.timestamp - a.timestamp)
118
- .slice(0, 10);
119
-
120
- const searchTrends = Array.from(dateCounts.entries())
121
- .map(([date, count]) => ({ date, count }))
122
- .sort((a, b) => a.date.localeCompare(b.date));
123
-
124
- return {
125
- totalSearches,
126
- avgPerDay,
127
- mostActiveHour,
128
- topSources,
129
- recentActivity,
130
- searchTrends,
131
- };
132
- }, [recentSearches, period, compact]);
133
-
134
- const getSourceColor = (source: string) => {
135
- const colors = {
136
- User: "blue",
137
- Followup: "green",
138
- Suggestion: "orange",
139
- Unknown: "gray",
140
- };
141
- return colors[source as keyof typeof colors] || "gray";
142
- };
143
-
144
- const formatHour = (hour: number) => {
145
- const period = hour >= 12 ? "PM" : "AM";
146
- const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
147
- return `${displayHour}:00 ${period}`;
148
- };
149
-
150
- if (isLoading) {
151
- return (
152
- <Card withBorder>
153
- <Center h={200}>
154
- <Text c="dimmed">Loading analytics...</Text>
155
- </Center>
156
- </Card>
157
- );
158
- }
159
-
160
- if (stats.totalSearches === 0) {
161
- return (
162
- <Card withBorder>
163
- <Center h={200}>
164
- <Stack align="center" gap="xs">
165
- <ThemeIcon size="xl" variant="light" color="gray">
166
- <IconSearch size={24} />
167
- </ThemeIcon>
168
- <Text c="dimmed">No search data available</Text>
169
- <Text size="sm" c="dimmed">
170
- Start searching to see analytics
171
- </Text>
172
- </Stack>
173
- </Center>
174
- </Card>
175
- );
176
- }
177
-
178
- function MetricCard({
179
- title,
180
- value,
181
- }: {
182
- title: string;
183
- value: string | number;
184
- }) {
185
- return (
186
- <Card withBorder p={compact ? "sm" : "md"}>
187
- <Stack gap="xs" align="flex-start">
188
- <Text size="xs" tt="uppercase" fw={700} c="dimmed">
189
- {title}
190
- </Text>
191
- <Text fw={700} size={compact ? "lg" : "xl"}>
192
- {value}
193
- </Text>
194
- </Stack>
195
- </Card>
196
- );
197
- }
198
-
199
- return (
200
- <Stack gap={compact ? "sm" : "md"}>
201
- <SimpleGrid cols={compact ? 2 : { base: 1, sm: 2, lg: 4 }}>
202
- <MetricCard
203
- title="Total Searches"
204
- value={stats.totalSearches.toLocaleString()}
205
- />
206
- <MetricCard title="Daily Average" value={stats.avgPerDay} />
207
- <MetricCard
208
- title="Most Active Hour"
209
- value={formatHour(stats.mostActiveHour)}
210
- />
211
- <MetricCard
212
- title="Last Search"
213
- value={
214
- stats.recentActivity.length > 0
215
- ? formatRelativeTime(stats.recentActivity[0].timestamp)
216
- : "Never"
217
- }
218
- />
219
- </SimpleGrid>
220
-
221
- <SimpleGrid cols={compact ? 1 : { base: 1, md: 2 }}>
222
- {!(compact && stats.topSources.length === 0) && (
223
- <Card withBorder>
224
- <Card.Section p={compact ? "sm" : "md"}>
225
- <Title order={compact ? 5 : 4} mb={compact ? "xs" : "md"}>
226
- Search Sources
227
- </Title>
228
-
229
- {stats.topSources.length > 0 ? (
230
- <Stack gap="xs">
231
- {stats.topSources.map((source) => (
232
- <Group key={source.source} justify="space-between">
233
- <Group gap="xs">
234
- <Badge
235
- color={getSourceColor(source.source)}
236
- variant="dot"
237
- size="sm"
238
- >
239
- {source.source}
240
- </Badge>
241
- <Text size={compact ? "xs" : "sm"}>
242
- {source.count} searches
243
- </Text>
244
- </Group>
245
-
246
- <Group gap="xs">
247
- <Progress
248
- value={source.percentage}
249
- size="sm"
250
- w={compact ? 40 : 60}
251
- color={getSourceColor(source.source)}
252
- />
253
- <Text size="xs" c="dimmed" w={30}>
254
- {source.percentage}%
255
- </Text>
256
- </Group>
257
- </Group>
258
- ))}
259
- </Stack>
260
- ) : (
261
- <Text c="dimmed" size="sm">
262
- No data available
263
- </Text>
264
- )}
265
- </Card.Section>
266
- </Card>
267
- )}
268
- </SimpleGrid>
269
-
270
- {stats.searchTrends.length > 1 && (
271
- <Card withBorder>
272
- <Card.Section p="md">
273
- <Title order={4} mb="md">
274
- Recent activity
275
- </Title>
276
- <Stack gap="xs" mt="md">
277
- {stats.searchTrends.slice(-7).map((trend) => {
278
- const maxCount = Math.max(
279
- ...stats.searchTrends.map((t) => t.count),
280
- );
281
- const percentage =
282
- maxCount > 0 ? (trend.count / maxCount) * 100 : 0;
283
-
284
- return (
285
- <Group key={trend.date} justify="space-between">
286
- <Text size="xs" c="dimmed" w={80}>
287
- {new Date(trend.date).toLocaleDateString("en", {
288
- month: "short",
289
- day: "numeric",
290
- })}
291
- </Text>
292
-
293
- <Group gap="xs" style={{ flex: 1 }}>
294
- <Progress
295
- value={percentage}
296
- size="sm"
297
- style={{ flex: 1 }}
298
- color="blue"
299
- />
300
- <Text size="xs" w={20}>
301
- {trend.count}
302
- </Text>
303
- </Group>
304
- </Group>
305
- );
306
- })}
307
- </Stack>
308
- </Card.Section>
309
- </Card>
310
- )}
311
- </Stack>
312
- );
313
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
client/components/App/App.tsx CHANGED
@@ -1,66 +1,43 @@
1
- import {
2
- Center,
3
- Container,
4
- Loader,
5
- MantineProvider,
6
- Stack,
7
- Text,
8
- } from "@mantine/core";
9
  import { Route, Switch } from "wouter";
10
  import "@mantine/core/styles.css";
11
  import { Notifications } from "@mantine/notifications";
12
  import { usePubSub } from "create-pubsub/react";
13
  import { lazy, useEffect, useState } from "react";
14
- import { addLogEntry } from "@/modules/logEntries";
15
- import { settingsPubSub } from "@/modules/pubSub";
16
- import { defaultSettings } from "@/modules/settings";
17
  import "@mantine/notifications/styles.css";
18
- import { verifyStoredAccessKey } from "@/modules/accessKey";
19
  import MainPage from "../Pages/Main/MainPage";
20
 
21
  const AccessPage = lazy(() => import("../Pages/AccessPage"));
22
 
23
- /**
24
- * Main application component with access key validation and routing
25
- */
26
- function App() {
27
  useInitializeSettings();
28
  const { hasValidatedAccessKey, isCheckingStoredKey, setValidatedAccessKey } =
29
  useAccessKeyValidation();
30
 
 
 
 
 
31
  return (
32
  <MantineProvider defaultColorScheme="dark">
33
- {isCheckingStoredKey ? (
34
- <Container h="100vh">
35
- <Center h="100vh">
36
- <Stack align="center">
37
- <Loader />
38
- <Text>Verifying access...</Text>
39
- </Stack>
40
- </Center>
41
- </Container>
42
- ) : (
43
- <>
44
- <Notifications />
45
- <Switch>
46
- <Route path="/">
47
- {hasValidatedAccessKey ? (
48
- <MainPage />
49
- ) : (
50
- <AccessPage
51
- onAccessKeyValid={() => setValidatedAccessKey(true)}
52
- />
53
- )}
54
- </Route>
55
- </Switch>
56
- </>
57
- )}
58
  </MantineProvider>
59
  );
60
  }
61
 
62
- export default App;
63
-
64
  /**
65
  * A custom React hook that initializes the application settings.
66
  *
@@ -96,18 +73,18 @@ function useInitializeSettings() {
96
  * @returns An object containing the validation state and loading state
97
  */
98
  function useAccessKeyValidation() {
99
- const [state, setState] = useState(() => ({
100
- hasValidatedAccessKey: !VITE_ACCESS_KEYS_ENABLED,
101
- isCheckingStoredKey: VITE_ACCESS_KEYS_ENABLED,
102
- }));
103
 
104
  useEffect(() => {
105
- if (!VITE_ACCESS_KEYS_ENABLED) return;
106
-
107
  async function checkStoredAccessKey() {
108
- const isValid = await verifyStoredAccessKey();
109
- if (isValid)
110
- setState((prev) => ({ ...prev, hasValidatedAccessKey: true }));
 
 
111
  setState((prev) => ({ ...prev, isCheckingStoredKey: false }));
112
  }
113
 
 
1
+ import { MantineProvider } from "@mantine/core";
 
 
 
 
 
 
 
2
  import { Route, Switch } from "wouter";
3
  import "@mantine/core/styles.css";
4
  import { Notifications } from "@mantine/notifications";
5
  import { usePubSub } from "create-pubsub/react";
6
  import { lazy, useEffect, useState } from "react";
7
+ import { addLogEntry } from "../../modules/logEntries";
8
+ import { settingsPubSub } from "../../modules/pubSub";
9
+ import { defaultSettings } from "../../modules/settings";
10
  import "@mantine/notifications/styles.css";
11
+ import { verifyStoredAccessKey } from "../../modules/accessKey";
12
  import MainPage from "../Pages/Main/MainPage";
13
 
14
  const AccessPage = lazy(() => import("../Pages/AccessPage"));
15
 
16
+ export function App() {
 
 
 
17
  useInitializeSettings();
18
  const { hasValidatedAccessKey, isCheckingStoredKey, setValidatedAccessKey } =
19
  useAccessKeyValidation();
20
 
21
+ if (isCheckingStoredKey) {
22
+ return null;
23
+ }
24
+
25
  return (
26
  <MantineProvider defaultColorScheme="dark">
27
+ <Notifications />
28
+ <Switch>
29
+ <Route path="/">
30
+ {VITE_ACCESS_KEYS_ENABLED && !hasValidatedAccessKey ? (
31
+ <AccessPage onAccessKeyValid={() => setValidatedAccessKey(true)} />
32
+ ) : (
33
+ <MainPage />
34
+ )}
35
+ </Route>
36
+ </Switch>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  </MantineProvider>
38
  );
39
  }
40
 
 
 
41
  /**
42
  * A custom React hook that initializes the application settings.
43
  *
 
73
  * @returns An object containing the validation state and loading state
74
  */
75
  function useAccessKeyValidation() {
76
+ const [state, setState] = useState({
77
+ hasValidatedAccessKey: false,
78
+ isCheckingStoredKey: true,
79
+ });
80
 
81
  useEffect(() => {
 
 
82
  async function checkStoredAccessKey() {
83
+ if (VITE_ACCESS_KEYS_ENABLED) {
84
+ const isValid = await verifyStoredAccessKey();
85
+ if (isValid)
86
+ setState((prev) => ({ ...prev, hasValidatedAccessKey: true }));
87
+ }
88
  setState((prev) => ({ ...prev, isCheckingStoredKey: false }));
89
  }
90
 
client/components/Logs/LogsModal.tsx CHANGED
@@ -13,7 +13,7 @@ import {
13
  import { IconInfoCircle, IconSearch } from "@tabler/icons-react";
14
  import { usePubSub } from "create-pubsub/react";
15
  import { useCallback, useEffect, useMemo, useState } from "react";
16
- import { logEntriesPubSub } from "@/modules/logEntries";
17
 
18
  export default function LogsModal({
19
  opened,
@@ -112,8 +112,8 @@ export default function LogsModal({
112
  </Table.Tr>
113
  </Table.Thead>
114
  <Table.Tbody>
115
- {logEntriesFromCurrentPage.map((entry) => (
116
- <Table.Tr key={entry.id}>
117
  <Table.Td>
118
  {new Date(entry.timestamp).toLocaleTimeString()}
119
  </Table.Td>
 
13
  import { IconInfoCircle, IconSearch } from "@tabler/icons-react";
14
  import { usePubSub } from "create-pubsub/react";
15
  import { useCallback, useEffect, useMemo, useState } from "react";
16
+ import { logEntriesPubSub } from "../../modules/logEntries";
17
 
18
  export default function LogsModal({
19
  opened,
 
112
  </Table.Tr>
113
  </Table.Thead>
114
  <Table.Tbody>
115
+ {logEntriesFromCurrentPage.map((entry, index) => (
116
+ <Table.Tr key={`${entry.timestamp}-${index}`}>
117
  <Table.Td>
118
  {new Date(entry.timestamp).toLocaleTimeString()}
119
  </Table.Td>
client/components/Logs/ShowLogsButton.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import { Button, Center, Loader, Stack, Text } from "@mantine/core";
2
  import { lazy, Suspense, useState } from "react";
3
- import { addLogEntry } from "@/modules/logEntries";
4
 
5
  const LogsModal = lazy(() => import("./LogsModal"));
6
 
 
1
  import { Button, Center, Loader, Stack, Text } from "@mantine/core";
2
  import { lazy, Suspense, useState } from "react";
3
+ import { addLogEntry } from "../../modules/logEntries";
4
 
5
  const LogsModal = lazy(() => import("./LogsModal"));
6
 
client/components/Pages/AccessPage.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import { Button, Container, Stack, TextInput, Title } from "@mantine/core";
2
  import { type FormEvent, useState } from "react";
3
- import { validateAccessKey } from "@/modules/accessKey";
4
- import { addLogEntry } from "@/modules/logEntries";
5
 
6
  interface AccessPageState {
7
  accessKey: string;
 
1
  import { Button, Container, Stack, TextInput, Title } from "@mantine/core";
2
  import { type FormEvent, useState } from "react";
3
+ import { validateAccessKey } from "../../modules/accessKey";
4
+ import { addLogEntry } from "../../modules/logEntries";
5
 
6
  interface AccessPageState {
7
  accessKey: string;
client/components/Pages/Main/MainPage.test.tsx DELETED
@@ -1,110 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- describe("MainPage component logic", () => {
4
- it("should show results only when query is not empty", () => {
5
- const isQueryEmpty = (query: string) => query.length === 0;
6
-
7
- expect(isQueryEmpty("")).toBe(true);
8
- expect(isQueryEmpty("test query")).toBe(false);
9
- expect(isQueryEmpty(" ")).toBe(false);
10
- });
11
-
12
- it("should determine when to show search results section", () => {
13
- const shouldShowSearchResults = (
14
- textSearchState: string,
15
- imageSearchState: string,
16
- ) => textSearchState !== "idle" || imageSearchState !== "idle";
17
-
18
- expect(shouldShowSearchResults("idle", "idle")).toBe(false);
19
- expect(shouldShowSearchResults("loading", "idle")).toBe(true);
20
- expect(shouldShowSearchResults("idle", "loading")).toBe(true);
21
- expect(shouldShowSearchResults("success", "success")).toBe(true);
22
- });
23
-
24
- it("should determine when to show AI response section", () => {
25
- const shouldShowAiResponse = (
26
- textGenerationState: string,
27
- showEnableAiResponsePrompt: boolean,
28
- ) => !showEnableAiResponsePrompt && textGenerationState !== "idle";
29
-
30
- expect(shouldShowAiResponse("idle", false)).toBe(false);
31
- expect(shouldShowAiResponse("generating", false)).toBe(true);
32
- expect(shouldShowAiResponse("generating", true)).toBe(false);
33
- expect(shouldShowAiResponse("complete", false)).toBe(true);
34
- });
35
-
36
- it("should determine when to show enable AI prompt", () => {
37
- const shouldShowEnablePrompt = (showEnableAiResponsePrompt: boolean) =>
38
- showEnableAiResponsePrompt;
39
-
40
- expect(shouldShowEnablePrompt(true)).toBe(true);
41
- expect(shouldShowEnablePrompt(false)).toBe(false);
42
- });
43
-
44
- it("should combine conditions correctly for full page state", () => {
45
- interface PageState {
46
- query: string;
47
- textSearchState: string;
48
- imageSearchState: string;
49
- textGenerationState: string;
50
- showEnableAiResponsePrompt: boolean;
51
- }
52
-
53
- const getVisibleSections = (state: PageState) => {
54
- const isQueryEmpty = state.query.length === 0;
55
- return {
56
- showResults:
57
- !isQueryEmpty &&
58
- (state.textSearchState !== "idle" ||
59
- state.imageSearchState !== "idle"),
60
- showAiResponse:
61
- !isQueryEmpty &&
62
- !state.showEnableAiResponsePrompt &&
63
- state.textGenerationState !== "idle",
64
- showEnablePrompt: !isQueryEmpty && state.showEnableAiResponsePrompt,
65
- };
66
- };
67
-
68
- expect(
69
- getVisibleSections({
70
- query: "",
71
- textSearchState: "success",
72
- imageSearchState: "idle",
73
- textGenerationState: "generating",
74
- showEnableAiResponsePrompt: false,
75
- }),
76
- ).toEqual({
77
- showResults: false,
78
- showAiResponse: false,
79
- showEnablePrompt: false,
80
- });
81
-
82
- expect(
83
- getVisibleSections({
84
- query: "test",
85
- textSearchState: "success",
86
- imageSearchState: "idle",
87
- textGenerationState: "generating",
88
- showEnableAiResponsePrompt: false,
89
- }),
90
- ).toEqual({
91
- showResults: true,
92
- showAiResponse: true,
93
- showEnablePrompt: false,
94
- });
95
-
96
- expect(
97
- getVisibleSections({
98
- query: "test",
99
- textSearchState: "idle",
100
- imageSearchState: "idle",
101
- textGenerationState: "idle",
102
- showEnableAiResponsePrompt: true,
103
- }),
104
- ).toEqual({
105
- showResults: false,
106
- showAiResponse: false,
107
- showEnablePrompt: true,
108
- });
109
- });
110
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
client/components/Pages/Main/MainPage.tsx CHANGED
@@ -1,25 +1,25 @@
1
  import { Container, Stack } from "@mantine/core";
2
  import { usePubSub } from "create-pubsub/react";
3
  import { lazy, Suspense } from "react";
4
- import SearchForm from "@/components/Search/Form/SearchForm";
5
  import {
6
  imageSearchStatePubSub,
7
  queryPubSub,
8
  settingsPubSub,
9
  textGenerationStatePubSub,
10
  textSearchStatePubSub,
11
- } from "@/modules/pubSub";
12
- import { searchAndRespond } from "@/modules/textGeneration";
 
13
  import MenuButton from "./Menu/MenuButton";
14
 
15
  const AiResponseSection = lazy(
16
- () => import("@/components/AiResponse/AiResponseSection"),
17
  );
18
  const SearchResultsSection = lazy(
19
- () => import("@/components/Search/Results/SearchResultsSection"),
20
  );
21
  const EnableAiResponsePrompt = lazy(
22
- () => import("@/components/AiResponse/EnableAiResponsePrompt"),
23
  );
24
 
25
  export default function MainPage() {
 
1
  import { Container, Stack } from "@mantine/core";
2
  import { usePubSub } from "create-pubsub/react";
3
  import { lazy, Suspense } from "react";
 
4
  import {
5
  imageSearchStatePubSub,
6
  queryPubSub,
7
  settingsPubSub,
8
  textGenerationStatePubSub,
9
  textSearchStatePubSub,
10
+ } from "../../../modules/pubSub";
11
+ import { searchAndRespond } from "../../../modules/textGeneration";
12
+ import SearchForm from "../../Search/Form/SearchForm";
13
  import MenuButton from "./Menu/MenuButton";
14
 
15
  const AiResponseSection = lazy(
16
+ () => import("../../AiResponse/AiResponseSection"),
17
  );
18
  const SearchResultsSection = lazy(
19
+ () => import("../../Search/Results/SearchResultsSection"),
20
  );
21
  const EnableAiResponsePrompt = lazy(
22
+ () => import("../../AiResponse/EnableAiResponsePrompt"),
23
  );
24
 
25
  export default function MainPage() {
client/components/Pages/Main/Menu/AISettings/AISettingsForm.tsx CHANGED
@@ -2,9 +2,12 @@ import { Select, Slider, Stack, Switch, Text, TextInput } from "@mantine/core";
2
  import { useForm } from "@mantine/form";
3
  import { usePubSub } from "create-pubsub/react";
4
  import { useMemo } from "react";
5
- import { settingsPubSub } from "@/modules/pubSub";
6
- import { defaultSettings, inferenceTypes } from "@/modules/settings";
7
- import { isWebGPUAvailable } from "@/modules/webGpu";
 
 
 
8
  import { AIParameterSlider } from "./components/AIParameterSlider";
9
  import { BrowserSettings } from "./components/BrowserSettings";
10
  import { HordeSettings } from "./components/HordeSettings";
 
2
  import { useForm } from "@mantine/form";
3
  import { usePubSub } from "create-pubsub/react";
4
  import { useMemo } from "react";
5
+ import { settingsPubSub } from "../../../../../modules/pubSub";
6
+ import {
7
+ defaultSettings,
8
+ inferenceTypes,
9
+ } from "../../../../../modules/settings";
10
+ import { isWebGPUAvailable } from "../../../../../modules/webGpu";
11
  import { AIParameterSlider } from "./components/AIParameterSlider";
12
  import { BrowserSettings } from "./components/BrowserSettings";
13
  import { HordeSettings } from "./components/HordeSettings";
client/components/Pages/Main/Menu/AISettings/components/AIParameterSlider.tsx CHANGED
@@ -1,13 +1,6 @@
1
  import { Slider, Stack, Text } from "@mantine/core";
2
  import type { AIParameterSliderProps } from "../types";
3
 
4
- /**
5
- * Slider component for AI parameters with label and description
6
- * @param label - The parameter label
7
- * @param description - Parameter description text
8
- * @param defaultValue - Default value for the parameter
9
- * @param props - Additional props to pass to Slider component
10
- */
11
  export const AIParameterSlider = ({
12
  label,
13
  description,
 
1
  import { Slider, Stack, Text } from "@mantine/core";
2
  import type { AIParameterSliderProps } from "../types";
3
 
 
 
 
 
 
 
 
4
  export const AIParameterSlider = ({
5
  label,
6
  description,
client/components/Pages/Main/Menu/AISettings/components/BrowserSettings.tsx CHANGED
@@ -1,29 +1,20 @@
1
  import { NumberInput, Skeleton, Switch } from "@mantine/core";
2
  import type { UseFormReturnType } from "@mantine/form";
3
  import { lazy, Suspense } from "react";
4
- import type { defaultSettings } from "@/modules/settings";
5
 
6
  const WebLlmModelSelect = lazy(
7
- () => import("@/components/AiResponse/WebLlmModelSelect"),
8
  );
9
  const WllamaModelSelect = lazy(
10
- () => import("@/components/AiResponse/WllamaModelSelect"),
11
  );
12
 
13
- /**
14
- * Props for the BrowserSettings component
15
- */
16
  interface BrowserSettingsProps {
17
- /** Form instance for managing browser AI settings */
18
  form: UseFormReturnType<typeof defaultSettings>;
19
- /** Whether WebGPU is available in the current browser */
20
  isWebGPUAvailable: boolean;
21
  }
22
 
23
- /**
24
- * Component for managing browser-based AI settings.
25
- * Provides controls for WebGPU/CPU selection, model selection, and CPU thread configuration.
26
- */
27
  export const BrowserSettings = ({
28
  form,
29
  isWebGPUAvailable,
 
1
  import { NumberInput, Skeleton, Switch } from "@mantine/core";
2
  import type { UseFormReturnType } from "@mantine/form";
3
  import { lazy, Suspense } from "react";
4
+ import type { defaultSettings } from "../../../../../../modules/settings";
5
 
6
  const WebLlmModelSelect = lazy(
7
+ () => import("../../../../../../components/AiResponse/WebLlmModelSelect"),
8
  );
9
  const WllamaModelSelect = lazy(
10
+ () => import("../../../../../../components/AiResponse/WllamaModelSelect"),
11
  );
12
 
 
 
 
13
  interface BrowserSettingsProps {
 
14
  form: UseFormReturnType<typeof defaultSettings>;
 
15
  isWebGPUAvailable: boolean;
16
  }
17
 
 
 
 
 
18
  export const BrowserSettings = ({
19
  form,
20
  isWebGPUAvailable,
client/components/Pages/Main/Menu/AISettings/components/HordeSettings.tsx CHANGED
@@ -1,25 +1,15 @@
1
  import { Select, TextInput } from "@mantine/core";
2
  import type { UseFormReturnType } from "@mantine/form";
3
- import type { defaultSettings } from "@/modules/settings";
4
- import { aiHordeDefaultApiKey } from "@/modules/textGenerationWithHorde";
5
  import type { HordeUserInfo, ModelOption } from "../types";
6
 
7
- /**
8
- * Props for the HordeSettings component
9
- */
10
  interface HordeSettingsProps {
11
- /** Form instance for managing Horde AI settings */
12
  form: UseFormReturnType<typeof defaultSettings>;
13
- /** User information from AI Horde, or null if not logged in */
14
  hordeUserInfo: HordeUserInfo | null;
15
- /** Available models from AI Horde */
16
  hordeModels: ModelOption[];
17
  }
18
 
19
- /**
20
- * Component for managing AI Horde settings.
21
- * Provides controls for API key input and model selection.
22
- */
23
  export const HordeSettings = ({
24
  form,
25
  hordeUserInfo,
 
1
  import { Select, TextInput } from "@mantine/core";
2
  import type { UseFormReturnType } from "@mantine/form";
3
+ import type { defaultSettings } from "../../../../../../modules/settings";
4
+ import { aiHordeDefaultApiKey } from "../../../../../../modules/textGenerationWithHorde";
5
  import type { HordeUserInfo, ModelOption } from "../types";
6
 
 
 
 
7
  interface HordeSettingsProps {
 
8
  form: UseFormReturnType<typeof defaultSettings>;
 
9
  hordeUserInfo: HordeUserInfo | null;
 
10
  hordeModels: ModelOption[];
11
  }
12
 
 
 
 
 
13
  export const HordeSettings = ({
14
  form,
15
  hordeUserInfo,
client/components/Pages/Main/Menu/AISettings/components/OpenAISettings.tsx CHANGED
@@ -1,25 +1,15 @@
1
- import { Group, NumberInput, Select, Text, TextInput } from "@mantine/core";
2
  import type { UseFormReturnType } from "@mantine/form";
3
  import { IconInfoCircle } from "@tabler/icons-react";
4
- import { defaultSettings } from "@/modules/settings";
5
  import type { ModelOption } from "../types";
6
 
7
- /**
8
- * Props for the OpenAISettings component
9
- */
10
  interface OpenAISettingsProps {
11
- /** Form instance for managing OpenAI API settings */
12
  form: UseFormReturnType<typeof defaultSettings>;
13
- /** Available OpenAI-compatible models */
14
  openAiModels: ModelOption[];
15
- /** Whether to use text input instead of select for model */
16
  useTextInput: boolean;
17
  }
18
 
19
- /**
20
- * Component for managing OpenAI API settings.
21
- * Provides controls for API base URL, API key, model selection, and context length.
22
- */
23
  export const OpenAISettings = ({
24
  form,
25
  openAiModels,
@@ -61,16 +51,7 @@ export const OpenAISettings = ({
61
  allowDeselect={false}
62
  disabled={openAiModels.length === 0}
63
  searchable
64
- clearable
65
  />
66
  )}
67
- <NumberInput
68
- label="Context Length"
69
- description={`Maximum number of tokens the model can consider. Defaults to ${defaultSettings.openAiContextLength}.`}
70
- defaultValue={defaultSettings.openAiContextLength}
71
- {...form.getInputProps("openAiContextLength")}
72
- step={defaultSettings.openAiContextLength}
73
- min={defaultSettings.openAiContextLength}
74
- />
75
  </>
76
  );
 
1
+ import { Group, Select, Text, TextInput } from "@mantine/core";
2
  import type { UseFormReturnType } from "@mantine/form";
3
  import { IconInfoCircle } from "@tabler/icons-react";
4
+ import type { defaultSettings } from "../../../../../../modules/settings";
5
  import type { ModelOption } from "../types";
6
 
 
 
 
7
  interface OpenAISettingsProps {
 
8
  form: UseFormReturnType<typeof defaultSettings>;
 
9
  openAiModels: ModelOption[];
 
10
  useTextInput: boolean;
11
  }
12
 
 
 
 
 
13
  export const OpenAISettings = ({
14
  form,
15
  openAiModels,
 
51
  allowDeselect={false}
52
  disabled={openAiModels.length === 0}
53
  searchable
 
54
  />
55
  )}
 
 
 
 
 
 
 
 
56
  </>
57
  );
client/components/Pages/Main/Menu/AISettings/components/SystemPromptInput.tsx CHANGED
@@ -1,26 +1,15 @@
1
  import { Text, Textarea } from "@mantine/core";
2
  import type { UseFormReturnType } from "@mantine/form";
3
- import { defaultSettings } from "@/modules/settings";
4
 
5
- /**
6
- * Props for the SystemPromptInput component
7
- */
8
  interface SystemPromptInputProps {
9
- /** Form instance for managing system prompt settings */
10
  form: UseFormReturnType<typeof defaultSettings>;
11
  }
12
 
13
- /**
14
- * Component for managing system prompt/instructions for AI.
15
- * Provides a textarea for customizing AI behavior with examples and restore functionality.
16
- */
17
  export const SystemPromptInput = ({ form }: SystemPromptInputProps) => {
18
  const isUsingCustomInstructions =
19
  form.values.systemPrompt !== defaultSettings.systemPrompt;
20
 
21
- /**
22
- * Handles restoring the default system prompt instructions
23
- */
24
  const handleRestoreDefaultInstructions = () => {
25
  form.setFieldValue("systemPrompt", defaultSettings.systemPrompt);
26
  };
@@ -79,8 +68,8 @@ export const SystemPromptInput = ({ form }: SystemPromptInputProps) => {
79
 
80
  <Text size="xs" component="span">
81
  The special tag <em>{"{{searchResults}}"}</em> will be replaced with
82
- the search results, while <em>{"{{currentDate}}"}</em> will be
83
- replaced with the current date.
84
  </Text>
85
 
86
  {isUsingCustomInstructions && (
 
1
  import { Text, Textarea } from "@mantine/core";
2
  import type { UseFormReturnType } from "@mantine/form";
3
+ import { defaultSettings } from "../../../../../../modules/settings";
4
 
 
 
 
5
  interface SystemPromptInputProps {
 
6
  form: UseFormReturnType<typeof defaultSettings>;
7
  }
8
 
 
 
 
 
9
  export const SystemPromptInput = ({ form }: SystemPromptInputProps) => {
10
  const isUsingCustomInstructions =
11
  form.values.systemPrompt !== defaultSettings.systemPrompt;
12
 
 
 
 
13
  const handleRestoreDefaultInstructions = () => {
14
  form.setFieldValue("systemPrompt", defaultSettings.systemPrompt);
15
  };
 
68
 
69
  <Text size="xs" component="span">
70
  The special tag <em>{"{{searchResults}}"}</em> will be replaced with
71
+ the search results, while <em>{"{{dateTime}}"}</em> will be replaced
72
+ with the current date and time.
73
  </Text>
74
 
75
  {isUsingCustomInstructions && (
client/components/Pages/Main/Menu/AISettings/hooks/useHordeModels.ts CHANGED
@@ -1,26 +1,15 @@
1
  import { useEffect, useState } from "react";
2
- import { addLogEntry } from "@/modules/logEntries";
3
- import type { defaultSettings } from "@/modules/settings";
4
- import { fetchHordeModels } from "@/modules/textGenerationWithHorde";
5
  import type { ModelOption } from "../types";
6
 
7
- /**
8
- * Type alias for the settings object
9
- */
10
  type Settings = typeof defaultSettings;
11
 
12
- /**
13
- * Hook for fetching and managing AI Horde models
14
- * @param settings - Application settings object
15
- * @returns Array of available AI Horde models formatted as ModelOption[]
16
- */
17
  export const useHordeModels = (settings: Settings) => {
18
  const [hordeModels, setHordeModels] = useState<ModelOption[]>([]);
19
 
20
  useEffect(() => {
21
- /**
22
- * Fetches available models from AI Horde and formats them for UI
23
- */
24
  async function fetchAvailableHordeModels() {
25
  try {
26
  const models = await fetchHordeModels();
 
1
  import { useEffect, useState } from "react";
2
+ import { addLogEntry } from "../../../../../../modules/logEntries";
3
+ import type { defaultSettings } from "../../../../../../modules/settings";
4
+ import { fetchHordeModels } from "../../../../../../modules/textGenerationWithHorde";
5
  import type { ModelOption } from "../types";
6
 
 
 
 
7
  type Settings = typeof defaultSettings;
8
 
 
 
 
 
 
9
  export const useHordeModels = (settings: Settings) => {
10
  const [hordeModels, setHordeModels] = useState<ModelOption[]>([]);
11
 
12
  useEffect(() => {
 
 
 
13
  async function fetchAvailableHordeModels() {
14
  try {
15
  const models = await fetchHordeModels();