Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions harvest-finance/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"zustand": "^5.0.11",
"react-toastify": "^9.1.3",
"next-intl": "^3.25.1"
,"fuse.js": "^6.6.2"
},
"devDependencies": {
"@jest/types": "^30.3.0",
Expand Down
37 changes: 37 additions & 0 deletions harvest-finance/frontend/scripts/validate-help.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const fs = require('fs');
const path = require('path');

const articles = JSON.parse(fs.readFileSync(path.join(__dirname, '../src/content/help/articles.json'), 'utf8'));

function simpleSearch(query, limit = 10) {
if (!query || query.trim() === '') return [];
const q = query.toLowerCase();
const out = articles.filter(a => {
return (
a.title.toLowerCase().includes(q) ||
a.body.toLowerCase().includes(q) ||
(a.keywords || []).some(k => k.toLowerCase().includes(q)) ||
a.category.toLowerCase().includes(q)
);
});
return out.slice(0, limit);
}

function assert(condition, msg) {
if (!condition) {
console.error('FAIL:', msg);
process.exitCode = 2;
return;
}
console.log('OK:', msg);
}

console.log('Validating help search...');
assert(articles.length > 0, 'articles loaded');
const r1 = simpleSearch('deposit');
assert(r1.length > 0, 'deposit query returns results');
assert(r1[0].title.toLowerCase().includes('deposit'), 'first result likely deposit article');
const r2 = simpleSearch('qwertyuiopzz');
assert(r2.length === 0, 'nonsense query returns no results');

console.log('All validations passed.');
17 changes: 17 additions & 0 deletions harvest-finance/frontend/src/__tests__/help-search.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { searchHelp, getAllArticles } from '../lib/help-search';

describe('help search', () => {
it('returns articles for common queries', () => {
const all = getAllArticles();
expect(all.length).toBeGreaterThan(0);

const res = searchHelp('deposit');
expect(res.length).toBeGreaterThan(0);
expect(res[0].title.toLowerCase()).toContain('deposit');
});

it('returns empty for unknown query', () => {
const res = searchHelp('qwertyuiopzz');
expect(res.length).toBe(0);
});
});
131 changes: 131 additions & 0 deletions harvest-finance/frontend/src/app/help/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"use client";

import React, { useEffect, useMemo, useState } from 'react';
import { searchHelp, getAllArticles, Article } from '../../../lib/help-search';

const NO_RESULTS_KEY = 'help_no_results';

function saveHelpful(articleId: string, helpful: boolean) {
const key = `helpful_${articleId}`;
try {
localStorage.setItem(key, helpful ? '1' : '0');
} catch (e) {}
}

function getHelpful(articleId: string) {
try {
const v = localStorage.getItem(`helpful_${articleId}`);
if (v === '1') return true;
if (v === '0') return false;
} catch (e) {}
return undefined;
}

function recordNoResults(query: string) {
try {
const raw = localStorage.getItem(NO_RESULTS_KEY);
const obj = raw ? JSON.parse(raw) : {};
obj[query] = (obj[query] || 0) + 1;
localStorage.setItem(NO_RESULTS_KEY, JSON.stringify(obj));
} catch (e) {}
}

export default function HelpPage() {
const all = useMemo(() => getAllArticles(), []);
const [query, setQuery] = useState('');
const [results, setResults] = useState<Article[] | null>(null);

useEffect(() => {
if (!query) {
setResults(null);
return;
}
const r = searchHelp(query, 20);
setResults(r);
if (r.length === 0) recordNoResults(query);
}, [query]);

const categories = useMemo(() => {
const map = new Map<string, Article[]>();
all.forEach(a => {
if (!map.has(a.category)) map.set(a.category, []);
map.get(a.category)!.push(a);
});
return Array.from(map.entries());
}, [all]);

return (
<div style={{ padding: 20 }}>
<h1>Help Centre</h1>
<p>Search our FAQs or browse by category.</p>
<input
aria-label="Search help"
placeholder="Search help articles"
value={query}
onChange={e => setQuery(e.target.value)}
style={{ width: '100%', padding: 8, marginBottom: 12 }}
/>

{results !== null ? (
<div>
<h2>Search results</h2>
{results.length === 0 ? (
<p>No results found for "{query}"</p>
) : (
results.map(a => <ArticleCard key={a.id} article={a} />)
)}
</div>
) : (
<div>
{categories.map(([cat, arts]) => (
<section key={cat} style={{ marginBottom: 16 }}>
<h3>{cat}</h3>
{arts.map(a => (
<ArticleCard key={a.id} article={a} />
))}
</section>
))}
</div>
)}
</div>
);
}

function ArticleCard({ article }: { article: Article }) {
const [helpful, setHelpful] = useState<boolean | undefined>(undefined);

useEffect(() => {
setHelpful(getHelpful(article.id));
}, [article.id]);

return (
<div style={{ border: '1px solid #eee', padding: 12, marginBottom: 8 }}>
<h4>{article.title}</h4>
<p>{article.body}</p>
<div style={{ marginTop: 8 }}>
<span>Was this helpful? </span>
<button
onClick={() => {
saveHelpful(article.id, true);
setHelpful(true);
}}
style={{ marginRight: 8 }}
aria-label={`helpful-yes-${article.id}`}
>
Yes
</button>
<button
onClick={() => {
saveHelpful(article.id, false);
setHelpful(false);
}}
aria-label={`helpful-no-${article.id}`}
>
No
</button>
{helpful === true && <span style={{ marginLeft: 8 }}>Thanks — helpful</span>}
{helpful === false && <span style={{ marginLeft: 8 }}>Thanks — we'll improve</span>}
</div>
</div>
);
}
37 changes: 37 additions & 0 deletions harvest-finance/frontend/src/content/help/articles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[
{
"id": "deposits-1",
"title": "How do I deposit funds into a vault?",
"category": "Deposits",
"body": "To deposit, navigate to the vault page, click Deposit, enter the amount and confirm the transaction with your wallet.",
"keywords": ["deposit","funds","vault","add funds"]
},
{
"id": "withdrawals-1",
"title": "How do withdrawals work?",
"category": "Withdrawals",
"body": "Withdrawals remove your funds from a vault. Depending on the strategy there may be a delay or on-chain fees.",
"keywords": ["withdraw","remove","vault","redeem"]
},
{
"id": "strategies-1",
"title": "What are vault strategies?",
"category": "Vault Strategies",
"body": "Vault strategies determine how deposited assets are allocated to earn yield. Strategies are managed by the protocol and can change over time.",
"keywords": ["strategy","yield","allocation","strategy change"]
},
{
"id": "wallets-1",
"title": "How do I connect my wallet?",
"category": "Wallet Connections",
"body": "Click the Connect button and choose your wallet provider. Approve the connection in the wallet popup.",
"keywords": ["wallet","connect","freighter","metamask"]
},
{
"id": "fees-1",
"title": "What fees does the protocol charge?",
"category": "Fees",
"body": "Fees may include withdrawal fees, performance fees, and protocol fees. Check the vault page for exact fee details.",
"keywords": ["fees","withdrawal fee","performance fee","protocol fee"]
}
]
50 changes: 50 additions & 0 deletions harvest-finance/frontend/src/lib/help-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
let Fuse: any = null;
try {
// try dynamic require so tests can run without installing fuse.js
// eslint-disable-next-line @typescript-eslint/no-var-requires
// @ts-ignore
Fuse = require('fuse.js');
} catch (e) {
Fuse = null;
}
import articles from '../content/help/articles.json';

export type Article = {
id: string;
title: string;
category: string;
body: string;
keywords?: string[];
};

const options = {
keys: ['title', 'body', 'keywords', 'category'],
includeScore: true,
threshold: 0.4,
};

export function searchHelp(query: string, limit = 10) {
if (!query || query.trim() === '') return [] as Article[];
if (Fuse) {
const fuse = new Fuse(articles as Article[], options);
const results = fuse.search(query, { limit });
return results.map((r: any) => r.item as Article);
}
// fallback simple substring search
const q = query.toLowerCase();
const out = (articles as Article[]).filter(a => {
return (
a.title.toLowerCase().includes(q) ||
a.body.toLowerCase().includes(q) ||
(a.keywords || []).some(k => k.toLowerCase().includes(q)) ||
a.category.toLowerCase().includes(q)
);
});
return out.slice(0, limit);
}

export function getAllArticles(): Article[] {
return articles as Article[];
}

export default { searchHelp, getAllArticles };