Made a working settings panel & includes the user's info, what is

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:
吳元皓 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 ## 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. 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"> <script setup lang="ts">
import { BadgeCheckIcon, OctagonAlertIcon } from "lucide-vue-next"; import { BadgeCheckIcon, OctagonAlertIcon } from "lucide-vue-next";
import { Input } from "~/components/ui/input"; 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 { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -62,10 +11,57 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; 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 showDeleteDialog = ref(false);
const showLogoutDialog = ref(false); const showLogoutDialog = ref(false);
const confirmDelete = async () => { const confirmDelete = async () => {
await deleteAccount(); await deleteAccount();
showDeleteDialog.value = false; showDeleteDialog.value = false;
@ -79,6 +75,23 @@ const deleteAccount = async () => {
method: "DELETE", 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, * userAccount: fetchViaSQL[0].username,
@ -87,11 +100,44 @@ const deleteAccount = async () => {
avatarURL: fetchViaSQL[0].avatarurl, avatarURL: fetchViaSQL[0].avatarurl,
firstName: fetchViaSQL[0].firstName, 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> </script>
<template> <template>
<div class="justify-center align-center text-center"> <div class="justify-center align-center text-center">
<h1 class="text-3xl text-bold p-2"> <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> </h1>
<div class="flex flex-row text-center align-center justify-center p-1"> <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> <span class="text-md p-1 text-nowrap">Change your name:&nbsp;</span>
@ -112,35 +158,37 @@ const deleteAccount = async () => {
/> />
<button <button
class="p-1 text-sm bg-gray-400/60 rounded text-nowrap" class="p-1 text-sm bg-gray-400/60 rounded text-nowrap"
@click="setFirstName" @click="submitChangeAction('NAME')"
:disabled="!enterFirstName"
> >
{{ t("settings.submit") }} {{ t("settings.submit") }}
</button> </button>
</div> </div>
<div class="flex flex-row text-center align-center justify-center p-1"> <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 class="text-md p-1 text-nowrap">Current email:&nbsp;</span>
<span>{{ useremail }}</span> <span>{{ useremail || "Oh, It's empty." }}</span>
</div> </div>
<div class="flex flex-row text-center align-center justify-center p-1"> <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> <span class="text-md p-1 text-nowrap">Change your email:&nbsp;</span>
<Input <Input
type="text" type="text"
class="h-6 m-1 py-3 rounded" class="h-6 m-1 py-3 rounded"
v-model="enterFirstName" v-model="enteruseremail"
placeholder="Ex: example@gmail.com" placeholder="Ex: example@gmail.com"
/> />
<!--If it is a valid api key or not.--> <!--If it is a valid api key or not.-->
<BadgeCheckIcon <BadgeCheckIcon
v-if="enterFirstName" v-if="enteruseremail"
class="w-8 h-8 p-1/2 mr-1 text-green-700" class="w-8 h-8 p-1/2 mr-1 text-green-700"
/> />
<OctagonAlertIcon <OctagonAlertIcon
v-if="!enterFirstName" v-if="!enteruseremail"
class="w-8 h-8 p-1/2 mr-1 text-red-700" class="w-8 h-8 p-1/2 mr-1 text-red-700"
/> />
<button <button
class="p-1 text-sm bg-gray-400/60 rounded text-nowrap" class="p-1 text-sm bg-gray-400/60 rounded text-nowrap"
@click="setFirstName" @click="submitChangeAction('USER_EMAIL')"
:disabled="!enteruseremail"
> >
{{ t("settings.submit") }} {{ t("settings.submit") }}
</button> </button>
@ -233,18 +281,20 @@ const deleteAccount = async () => {
> >
<button <button
class="bg-sky-400 p-1 rounded hover:bg-sky-600 transition-all duration-200 w-32" class="bg-sky-400 p-1 rounded hover:bg-sky-600 transition-all duration-200 w-32"
@click="emit('windowopener', 'privacypolicy')"
> >
Privacy Policy Privacy Policy
</button> </button>
<button <button
class="bg-sky-400 p-1 rounded hover:bg-sky-600 transition-all duration-200 w-32" class="bg-sky-400 p-1 rounded hover:bg-sky-600 transition-all duration-200 w-32"
@click="emit('windowopener', 'tos')"
> >
TOS TOS
</button> </button>
</div> </div>
<hr /> <hr />
<div class="justiy-center align-center text-center"> <div class="justiy-center align-center text-center">
{{ t("app.settings") }} v0.0.2 {{ t("app.settings") }} v0.0.3
</div> </div>
</div> </div>
</template> </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 // Confeti
const successcanvas = ref(); const successcanvas = ref();
const confetiActive = ref(false); const confetiActive = ref(false);

View File

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

View File

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

View File

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