diff --git a/src/app/app.component.html b/src/app/app.component.html index 991a6d6..639b126 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,34 +1,39 @@ -
+
diff --git a/src/app/pages/homepage/parts/stats/homepage-stats.component.html b/src/app/pages/homepage/parts/stats/homepage-stats.component.html index 0f661dd..30b780d 100644 --- a/src/app/pages/homepage/parts/stats/homepage-stats.component.html +++ b/src/app/pages/homepage/parts/stats/homepage-stats.component.html @@ -10,7 +10,7 @@

{{'image_stats' | transloco}}

diff --git a/src/app/pages/homepage/parts/stats/homepage-stats.component.ts b/src/app/pages/homepage/parts/stats/homepage-stats.component.ts index 56d6ead..50b74aa 100644 --- a/src/app/pages/homepage/parts/stats/homepage-stats.component.ts +++ b/src/app/pages/homepage/parts/stats/homepage-stats.component.ts @@ -52,7 +52,7 @@ export class HomepageStatsComponent implements OnInit, OnDestroy { this.subscriptions.unsubscribe(); } - public async ngOnInit(): Promise { + public ngOnInit(): void { if (this.isBrowser) { this.subscriptions.add(interval(60_000).pipe( startWith(0), @@ -64,10 +64,10 @@ export class HomepageStatsComponent implements OnInit, OnDestroy { this.interrogationStats.set(value[4]); })); } else { - this.stats.set(await toPromise(this.aiHorde.performance)); - this.imageStats.set((await toPromise(this.aiHorde.imageStats)).total); - this.textStats.set((await toPromise(this.aiHorde.textStats)).total); - this.interrogationStats.set(await toPromise(this.aiHorde.interrogationStats)); + toPromise(this.aiHorde.performance).then(value => this.stats.set(value)); + toPromise(this.aiHorde.imageStats).then(value => this.imageStats.set(value.total)); + toPromise(this.aiHorde.textStats).then(value => this.textStats.set(value.total)); + toPromise(this.aiHorde.interrogationStats).then(value => this.interrogationStats.set(value)); } } -} +} \ No newline at end of file diff --git a/src/app/pages/news/news.component.html b/src/app/pages/news/news.component.html index bced744..1db753a 100644 --- a/src/app/pages/news/news.component.html +++ b/src/app/pages/news/news.component.html @@ -1 +1,35 @@ -

news works!

+
+
+
+

+ +

+
+
+
+ +
+
+
+
+ @for (newsItem of news(); track newsItem.title) { +
+

+ {{newsItem.date_published}} - +

+ @if (newsItem.excerpt) { +

+ } + @if (newsItem.moreLink) { +

+ {{'read_more' | transloco}} +

+ } +
+ } @empty { +

{{'no_news' | transloco}}

+ } +
+
+
+
diff --git a/src/app/pages/news/news.component.ts b/src/app/pages/news/news.component.ts index bad17a5..6a8ea47 100644 --- a/src/app/pages/news/news.component.ts +++ b/src/app/pages/news/news.component.ts @@ -1,12 +1,32 @@ -import {Component} from '@angular/core'; +import {Component, OnInit, signal} from '@angular/core'; +import {NewsItem} from "../../types/news.types"; +import {AiHordeService} from "../../services/ai-horde.service"; +import {toPromise} from "../../types/resolvable"; +import {RouterLink} from "@angular/router"; +import {TranslocoMarkupComponent} from "ngx-transloco-markup"; +import {TranslocoPipe} from "@jsverse/transloco"; + @Component({ selector: 'app-news', standalone: true, - imports: [], + imports: [ + RouterLink, + TranslocoMarkupComponent, + TranslocoPipe, + ], templateUrl: './news.component.html', styleUrl: './news.component.scss' }) -export class NewsComponent { +export class NewsComponent implements OnInit { + public news = signal([]); + + constructor( + private readonly aiHorde: AiHordeService, + ) { + } -} + public async ngOnInit(): Promise { + this.news.set(await toPromise(this.aiHorde.getNews())); + } +} \ No newline at end of file diff --git a/src/app/pages/terms/terms.component.html b/src/app/pages/terms/terms.component.html index 7a8f256..559bb45 100644 --- a/src/app/pages/terms/terms.component.html +++ b/src/app/pages/terms/terms.component.html @@ -1 +1,6 @@ -

terms works!

+
+
+
+
+
+
diff --git a/src/app/pages/terms/terms.component.ts b/src/app/pages/terms/terms.component.ts index 08e594b..a3439f4 100644 --- a/src/app/pages/terms/terms.component.ts +++ b/src/app/pages/terms/terms.component.ts @@ -1,4 +1,7 @@ -import { Component } from '@angular/core'; +import { Component, signal, ViewEncapsulation } from '@angular/core'; +import {toPromise} from "../../types/resolvable"; + +import {AiHordeService} from "../../services/ai-horde.service"; @Component({ selector: 'app-terms', @@ -8,5 +11,15 @@ import { Component } from '@angular/core'; styleUrl: './terms.component.scss' }) export class TermsComponent { + public terms = signal(''); + + constructor( + private readonly aiHorde: AiHordeService, + ) { + } + + public async ngOnInit(): Promise { + this.terms.set(await toPromise(this.aiHorde.getTerms())); + } } diff --git a/src/app/services/ai-horde.service.ts b/src/app/services/ai-horde.service.ts index 20b19ea..0d92069 100644 --- a/src/app/services/ai-horde.service.ts +++ b/src/app/services/ai-horde.service.ts @@ -17,25 +17,25 @@ export class AiHordeService { } public get imageStats(): Observable { - return of({ - month: { images: 105150339, ps: 1553239485353984 }, - total: { images: 105150339, ps: 1553239485353984 }, - day: { images: 105150339, ps: 1553239485353984 }, - hour: { images: 105150339, ps: 1553239485353984 }, - minute: { images: 105150339, ps: 1553239485353984 }, - }); - // return this.httpClient.get('https://aihorde.net/api/v2/stats/img/totals'); + // return of({ + // month: { images: 105150339, ps: 1553239485353984 }, + // total: { images: 105150339, ps: 1553239485353984 }, + // day: { images: 105150339, ps: 1553239485353984 }, + // hour: { images: 105150339, ps: 1553239485353984 }, + // minute: { images: 105150339, ps: 1553239485353984 }, + // }); + return this.httpClient.get('https://aihorde.net/api/v2/stats/img/totals'); } public get textStats(): Observable { - return of({ - total: { requests: 111931745, tokens: 20444501084 }, - day: { requests: 111931745, tokens: 20444501084 }, - hour: { requests: 111931745, tokens: 20444501084 }, - minute: { requests: 111931745, tokens: 20444501084 }, - month: { requests: 111931745, tokens: 20444501084 }, - }); - // return this.httpClient.get('https://aihorde.net/api/v2/stats/text/totals'); + // return of({ + // total: { requests: 111931745, tokens: 20444501084 }, + // day: { requests: 111931745, tokens: 20444501084 }, + // hour: { requests: 111931745, tokens: 20444501084 }, + // minute: { requests: 111931745, tokens: 20444501084 }, + // month: { requests: 111931745, tokens: 20444501084 }, + // }); + return this.httpClient.get('https://aihorde.net/api/v2/stats/text/totals'); } public get performance(): Observable { @@ -48,9 +48,26 @@ export class AiHordeService { }); } - public getNews(count: number): Observable { - return this.httpClient.get('/assets/data/news.json').pipe( - map(items => items.slice(0, count)), + // The endpoint `https://aihorde.net/api/v2/documents/terms?format=html` returns the terms and conditions with a field "html" containing the HTML content. + public getTerms(): Observable { + return this.httpClient.get('https://aihorde.net/api/v2/documents/terms?format=html').pipe( + map(response => response.html) + ); + } + + public getNews(count?: number): Observable { + return this.httpClient.get('https://aihorde.net/api/v2/status/news').pipe( + map(newsItems => count ? newsItems.slice(0, count) : newsItems), + map(newsItems => newsItems.map(newsItem => { + const markdownLinkRegex = /\[([^\[]+)\]\(([^\)]+)\)/g; + const excerpt = newsItem.newspiece.replace(markdownLinkRegex, '$1'); + return { + title: newsItem.title, + date_published: newsItem.date_published, + excerpt: excerpt, + moreLink: newsItem.more_info_urls.length > 0 ? newsItem.more_info_urls[0] : null + }; + })) ); } -} +} \ No newline at end of file diff --git a/src/app/types/news.types.ts b/src/app/types/news.types.ts index 6ddbaab..1115490 100644 --- a/src/app/types/news.types.ts +++ b/src/app/types/news.types.ts @@ -1,5 +1,6 @@ export interface NewsItem { title: string; + date_published: string; excerpt: string | null; moreLink: string | null; } diff --git a/src/assets/data/image-guis.en.json b/src/assets/data/image-guis.en.json index 7b0c1a4..93a3694 100644 --- a/src/assets/data/image-guis.en.json +++ b/src/assets/data/image-guis.en.json @@ -2,7 +2,7 @@ { "name": "Lucid Creations", "description": "A client that works both in your browser and can be downloaded by the founder of the AI Horde project.", - "image": "/assets/img/guis/lucid-creations.png", + "image": "assets/img/guis/lucid-creations.png", "link": "https://dbzer0.itch.io/lucid-creations", "downloadButtonText": "Get {name}", "categories": ["Desktop", "Web"] @@ -10,28 +10,28 @@ { "name": "Artbot", "description": "One of the most popular web-based clients.", - "image": "/assets/img/guis/artbot.png", + "image": "assets/img/guis/artbot.png", "link": "https://tinybots.net/artbot", "categories": "Web" }, { "name": "HordeNG", "description": "HordeNG aims to be very simple to use for beginners, while allowing the full set of options for advanced users.", - "image": "/assets/img/guis/horde-ng.png", + "image": "assets/img/guis/horde-ng.png", "link": "https://horde-ng.org/generate", "categories": "Web" }, { "name": "AI Scribbles", "description": "A simplified web image generator with handmade custom styles", - "image": "/assets/img/guis/ai-scribbles.png", + "image": "assets/img/guis/ai-scribbles.png", "link": "https://www.aiscribbles.com/generate/", "categories": "Web" }, { "name": "AI Painter", "description": "A Chinese language app which has both a simple mode and advanced features.", - "image": "/assets/img/guis/ai-painter.jpg", + "image": "assets/img/guis/ai-painter.jpg", "link": "https://play.google.com/store/apps/details?id=wkygame.ai.all.in.one", "downloadButtonText": "Get {name}", "categories": "Android" @@ -39,7 +39,7 @@ { "name": "HordeNG", "description": "HordeNG aims to be very simple to use for beginners, while allowing the full set of options for advanced users.", - "image": "/assets/img/guis/horde-ng-android.jpg", + "image": "assets/img/guis/horde-ng-android.jpg", "link": "https://play.google.com/store/apps/details?id=org.horde_ng.twa", "downloadButtonText": "Get {name}", "categories": "Android" @@ -47,7 +47,7 @@ { "name": "AI Painter", "description": "A Chinese language app which has both a simple mode and advanced features.", - "image": "/assets/img/guis/ai-painter.jpg", + "image": "assets/img/guis/ai-painter.jpg", "link": "https://apps.apple.com/hk/app/%E6%A9%9F%E7%95%AB%E5%B8%AB-%E5%B0%88%E6%A5%AD%E7%9A%84ai%E7%B9%AA%E7%95%ABapp/id1644645946", "downloadButtonText": "Get {name}", "categories": "iOS" @@ -55,7 +55,7 @@ { "name": "Aislingeach", "description": "A iOS and Android app that allows you to generate images using AI, as well as rate images.", - "image": "/assets/img/guis/aislingeach.png", + "image": "assets/img/guis/aislingeach.png", "link": "https://github.com/amiantos/aislingeach", "downloadButtonText": "Get {name}", "categories": "iOS" diff --git a/src/assets/data/text-guis.en.json b/src/assets/data/text-guis.en.json index 5f42039..4c4d5b2 100644 --- a/src/assets/data/text-guis.en.json +++ b/src/assets/data/text-guis.en.json @@ -2,7 +2,7 @@ { "name": "KoboldAI Client", "description": "This is a browser-based front-end for AI-assisted writing with multiple local & remote AI models, including through the AI Horde.", - "image": "/assets/img/guis/koboldai.png", + "image": "assets/img/guis/koboldai.png", "link": "https://koboldai.org/", "downloadButtonText": "Get {name}", "categories": "Desktop" @@ -10,7 +10,7 @@ { "name": "SillyTavern", "description": "A downloadable client that allows you to interact with text generation AIs, including the AI Horde.", - "image": "/assets/img/guis/sillytavern.png", + "image": "assets/img/guis/sillytavern.png", "link": "https://sillytavernai.com/", "downloadButtonText": "Get {name}", "categories": "Desktop" @@ -18,21 +18,21 @@ { "name": "KoboldAI Lite", "description": "A simple web-based client for AI chat and roleplay with multiple AI models, including through the AI Horde.", - "image": "/assets/img/guis/koboldai-lite.png", + "image": "assets/img/guis/koboldai-lite.png", "link": "https://lite.koboldai.net", "categories": "Web" }, { "name": "AgnAIstic", "description": "AI chat with characters. Open source, self-hostable, and publicly hosted, including through the AI Horde.", - "image": "/assets/img/guis/agnaistic.png", + "image": "assets/img/guis/agnaistic.png", "link": "https://agnai.chat", "categories": "Web" }, { "name": "Inneal", "description": "An iOS app for LLM chat through the AI Horde. Has support for multi-character chat, personas and more.", - "image": "/assets/img/guis/inneal.png", + "image": "assets/img/guis/inneal.png", "link": "https://github.com/amiantos/inneal", "downloadButtonText": "Get {name}", "categories": "iOS" diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 18ea2f5..d5e4c70 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -35,6 +35,7 @@ "home": "Home", "faq": "FAQ", "news": "News", + "api": "API Docs", "open_main_menu": "Open main menu", "community": "Community", "discord": "Discord", @@ -54,7 +55,7 @@ "image_alchemy": "Image alchemy", "realtime_stats": "Realtime stats", "quickstart": "Quickstart", - "quickstart.register_account": "First [link:registerLink]register an account[/link] which will generate for you an API key. Store that key somewhere.", + "quickstart.register_account": "First register an account which will generate for you an API key. Store that key somewhere.", "quickstart.anonymous_api_key": "if you do not want to register, you can use [b]'0000000000'[/b] as an api key to connect anonymously. However anonymous accounts have the lowest priority when there's too many concurrent requests!", "quickstart.explanation_api_key": "To increase your priority you will need a unique API key and then to increase your Kudos. [link:kudosExplanationLink]Read how Kudos are working[/link].", "guis.image": "Image GUIs", diff --git a/src/assets/img/logo.png b/src/assets/img/logo.png index cbf9b4b..dbb6ac9 100755 Binary files a/src/assets/img/logo.png and b/src/assets/img/logo.png differ diff --git a/src/styles.scss b/src/styles.scss index 7c1ec4e..52d9bc1 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -43,3 +43,47 @@ h3, p, .underline-links { color: $purpleTextColor500; } } + +// The terms are dynamically loaded and due to encapsulation, we need to use the global styles +.terms{ + ul { + list-style-type: disc; + padding-left: 40px; + } + + + h1 { + font-size: 1.5em; + font-weight: bold; + margin-bottom: 20px; + margin-top: 40px; + border-bottom: 3px solid #a0a0a0; + display: inline-block; + } + + h2 { + font-size: 1.3em; + font-weight: bold; + margin-bottom: 20px; + } + + p { + font-size: 1em; + margin-bottom: 10px; + } + + strong { + font-weight: bold; + } + + .last_updated { + font-style: italic; + } + + .important { + font-weight: bold; + font-size: 1.2em; + margin-top: 30px; + margin-bottom: 30px; + } +} \ No newline at end of file