Made a working settings panel & includes the user's info, what is
Some checks are pending
Build and Push Docker Image / build-and-push (push) Waiting to run

missing? well, all the actions that requires the data to be sent to the
server is still not there yet. Tried to add onboarding, but I have just
no idea how to do it (Maybe I can do it w/ a video?
This commit is contained in:
yuanhau 2025-06-07 23:51:05 +08:00
parent 1eb15058d7
commit 29760dda96
6 changed files with 201 additions and 63 deletions

View file

@ -21,6 +21,10 @@ Reverse engineering 文檔: [about](/about/)
## Note for developing
The desktop enviroment is super unstable when even using a beefy computer, even so, the desktop will lag when opening the newsView, like it's just hates being in a dev env. Prod app works tho, so you can demo it using `bun run build && bun run preview` for demoing. Please don't file a issue request for this matter. If you have the fix, please contribute using Github PRs.
## 如果要開發,你需要
- 一個 Postgres 資料庫 (你可以用 Zeabur 跑開發用資料庫,可以用我的 [優惠連結(?](https://zeabur.com/referral?referralCode=hpware)你可以拿到大約150塊的試用金額
- 一個 Groq 的 API
## 為什麼?
我們使用這個新聞來舉例:

View file

@ -1,57 +1,6 @@
<script setup lang="ts">
import { BadgeCheckIcon, OctagonAlertIcon } from "lucide-vue-next";
import { Input } from "~/components/ui/input";
const { t, locale } = useI18n();
const user = ref("");
const enterFirstName = ref();
const useremail = ref();
const enteruseremail = ref();
onMounted(async () => {
const req = await fetch("/api/user/validateUserToken");
const res = await req.json();
user.value = res.firstName;
useremail.value = res;
});
const setFirstName = async () => {
const staticFirstName = "";
};
const logoutAction = () => {};
const groqApiKeyRegex = /^gsk_[a-zA-Z0-9]{52}$/;
const customApiKey = ref();
const isCorrect = ref(false);
const submitCustomApiKey = async () => {
if (!isCorrect.value) {
checkValidApiKey();
if (!isCorrect.value) {
return;
}
}
const apiKey = customApiKey.value;
try {
const sendApi = await fetch("/api/ai/loadCustomGroqApi", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
apiKey: apiKey,
}),
});
const data = await sendApi.json();
if (data.error) {
}
} catch (e) {}
};
const checkValidApiKey = () => {
const apiKey = customApiKey.value;
if (!apiKey) {
isCorrect.value = false;
return;
}
isCorrect.value = groqApiKeyRegex.test(apiKey);
};
import {
Dialog,
DialogContent,
@ -62,10 +11,57 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
const { t, locale } = useI18n();
const user = ref("");
const enterFirstName = ref();
const useremail = ref();
const userData = ref({
userAccount: "",
firstName: "",
requested_action: "",
email: "",
avatarURL: "",
firstName: "",
});
const enteruseremail = ref();
onMounted(async () => {
const req = await fetch("/api/user/validateUserToken");
const res = await req.json();
user.value = res.firstName;
userData.value = res;
useremail.value = res.email;
});
const setFirstName = async () => {
const staticFirstName = "";
};
const emit = defineEmits(["windowopener"]);
const logoutAction = () => {};
const groqApiKeyRegex = /^gsk_[a-zA-Z0-9]{52}$/;
const customApiKey = ref();
const isCorrect = ref(false);
const submitCustomApiKey = async () => {
if (!isCorrect.value) {
checkValidApiKey();
if (!isCorrect.value) {
return;
}
}
};
const checkValidApiKey = () => {
const apiKey = customApiKey.value;
if (!apiKey) {
isCorrect.value = false;
return;
}
isCorrect.value = groqApiKeyRegex.test(apiKey);
};
const showDeleteDialog = ref(false);
const showLogoutDialog = ref(false);
const confirmDelete = async () => {
await deleteAccount();
showDeleteDialog.value = false;
@ -79,6 +75,23 @@ const deleteAccount = async () => {
method: "DELETE",
});
};
const apiKey = customApiKey.value;
try {
const sendApi = await fetch("/api/ai/loadCustomGroqApi", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
apiKey: apiKey,
}),
});
const data = await sendApi.json();
if (data.error) {
}
} catch (e) {
console.log(e);
}
/**
*
* userAccount: fetchViaSQL[0].username,
@ -87,11 +100,44 @@ const deleteAccount = async () => {
avatarURL: fetchViaSQL[0].avatarurl,
firstName: fetchViaSQL[0].firstName,
*/
const actions = [
{ name: "NAME", sendValue: enterFirstName.value },
{ name: "USER_EMAIL", sendValue: enteruseremail.value },
];
const submitChangeAction = async (action: string) => {
const actionMatch = actions.find((a) => a.name === action);
if (!actionMatch) {
console.error("Invalid action type");
return;
}
try {
const req = await fetch("/api/user/sendUserChanges", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
action: actionMatch.name,
value: actionMatch.sendValue,
}),
});
const response = await req.json();
if (response.error) {
console.error("Error updating user data:", response.error);
}
} catch (error) {
console.error("Failed to submit change:", error);
}
};
</script>
<template>
<div class="justify-center align-center text-center">
<h1 class="text-3xl text-bold p-2">
{{ t("settings.greet") }}{{ user || t("settings.defaultname") }}
{{ t("settings.greet")
}}{{ user || userData.userAccount || t("settings.defaultname") }}
</h1>
<div class="flex flex-row text-center align-center justify-center p-1">
<span class="text-md p-1 text-nowrap">Change your name:&nbsp;</span>
@ -112,35 +158,37 @@ const deleteAccount = async () => {
/>
<button
class="p-1 text-sm bg-gray-400/60 rounded text-nowrap"
@click="setFirstName"
@click="submitChangeAction('NAME')"
:disabled="!enterFirstName"
>
{{ t("settings.submit") }}
</button>
</div>
<div class="flex flex-row text-center align-center justify-center p-1">
<span class="text-md p-1 text-nowrap">Current email:&nbsp;</span>
<span>{{ useremail }}</span>
<span>{{ useremail || "Oh, It's empty." }}</span>
</div>
<div class="flex flex-row text-center align-center justify-center p-1">
<span class="text-md p-1 text-nowrap">Change your email:&nbsp;</span>
<Input
type="text"
class="h-6 m-1 py-3 rounded"
v-model="enterFirstName"
v-model="enteruseremail"
placeholder="Ex: example@gmail.com"
/>
<!--If it is a valid api key or not.-->
<BadgeCheckIcon
v-if="enterFirstName"
v-if="enteruseremail"
class="w-8 h-8 p-1/2 mr-1 text-green-700"
/>
<OctagonAlertIcon
v-if="!enterFirstName"
v-if="!enteruseremail"
class="w-8 h-8 p-1/2 mr-1 text-red-700"
/>
<button
class="p-1 text-sm bg-gray-400/60 rounded text-nowrap"
@click="setFirstName"
@click="submitChangeAction('USER_EMAIL')"
:disabled="!enteruseremail"
>
{{ t("settings.submit") }}
</button>
@ -233,18 +281,20 @@ const deleteAccount = async () => {
>
<button
class="bg-sky-400 p-1 rounded hover:bg-sky-600 transition-all duration-200 w-32"
@click="emit('windowopener', 'privacypolicy')"
>
Privacy Policy
</button>
<button
class="bg-sky-400 p-1 rounded hover:bg-sky-600 transition-all duration-200 w-32"
@click="emit('windowopener', 'tos')"
>
TOS
</button>
</div>
<hr />
<div class="justiy-center align-center text-center">
{{ t("app.settings") }} v0.0.2
{{ t("app.settings") }} v0.0.3
</div>
</div>
</template>

View file

@ -220,6 +220,88 @@ const associAppWindow = [
},
];
// OnBoarding
// Feedback from: https://hackclub.slack.com/archives/C090DPG6681/p1749303838738019
const currentStep = ref(0);
const showOnboarding = ref(true);
onMounted(() => {
showOnboarding.value = !localStorage.getItem("onboardingComplete");
});
const nextStep = () => {
currentStep.value++;
};
const finishOnboarding = () => {
showOnboarding.value = false;
localStorage.setItem("onboardingComplete", "true");
};
/*const onBoarding = [
{
step: 0,
point: "none",
text: "Hi! Welcome to the news analyze desktop enviroment!",
buttons: [
"bypass": nextStep,
"contuine": nextStep
]
},
{
step: 1,
point: "top-left",
text: "Click here to open applications",
buttons: [
"ok": nextStep
]
},
{
step: 2,
point: "left-navbar-1",
text: "Click here to open the news window",
buttons: [
"ok": nextStep
]
},
{
step: 3,
point: "center",
text: "Click here open a news article",
buttons: [
"ok": nextStep
]
},
{
step: 4,
point: "center-close-translate-left",
text: "Click here to translate the page.",
buttons: [
"ok": nextStep
]
},
{
step: 5,
point: "center-close-x-left",
text: "Click here to close the window",
buttons: [
"ok": nextStep
]
},
{
step: 6,
point: "more-top-right-3",
text: "Click here to change the app's language. (YOU WILL LOSE ALL YOUR WINDOWS)",
buttons: [
"ok": nextStep
]
},
{
step: 7,
point: "none",
text: "That's it, welcome! If you want to learn more, you can go to yhw.tw/newsanalyzedocs.",
buttons: [
"ok": finishOnboarding
]
},
];*/
// Confeti
const successcanvas = ref();
const confetiActive = ref(false);

View file

@ -1,7 +1,7 @@
import sql from "~/server/components/postgres";
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const { request_change } = body;
/*
const userToken = getCookie(event, "token");
if (!userToken) {
return {
@ -19,5 +19,6 @@ export default defineEventHandler(async (event) => {
}
if (request_change === "groq_api_key") {
const updateListing = await sql``;
}
}*/
return { body: body };
});

View file

@ -39,6 +39,7 @@ export default defineEventHandler(async (event) => {
}
return {
userAccount: fetchViaSQL[0].username,
firstName: fetchViaSQL[0].firstName,
requested_action: "CONTINUE",
email: fetchViaSQL[0].email,
avatarURL: fetchViaSQL[0].avatarurl,

View file

@ -19,7 +19,7 @@ export async function checkIfUserHasCustomGroqKey(token?: string) {
}
const fetchUserToken = await sql`
select groq_api_key from user_other_data
where username=${checkRealToken[0].username}`;
where username = ${checkRealToken[0].username}`;
if (fetchUserToken.length === 0) {
return {
status: false,