first commit

This commit is contained in:
2026-01-09 21:25:34 +09:00
parent c5b6c019e6
commit 891ce6f3a9
36 changed files with 1391 additions and 46 deletions

7
.idea/misc.xml generated
View File

@@ -6,4 +6,11 @@
<component name="ProjectType">
<option name="id" value="Android" />
</component>
<component name="VisualizationToolProject">
<option name="state">
<ProjectState>
<option name="scale" value="0.201519775390625" />
</ProjectState>
</option>
</component>
</project>

View File

@@ -51,6 +51,16 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-simplexml:2.9.0")
dependencies {
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-simplexml:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
}
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -22,6 +25,12 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".RegionSelectActivity" />
<activity android:name=".LineSelectActivity" />
<activity android:name=".LinePriorityActivity" />
</application>
</manifest>

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class AkaneResponder : CharacterResponder {
override val profile = CharacterProfiles.akane
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"えっ!?遅延!? 急ぐなら別ルートも考えたほうがいいかもだよっ!!"
InfoType.REVISION ->
"うわーっ!ダイヤ改正だって!? 新しい運用とかワクワクするじゃんっ!"
InfoType.EVENT ->
"イベント列車!? こういうのめっちゃ楽しみだよねっ!!"
else ->
"なんか気になる情報があるよねっ!気をつけていこーっ!"
}
}
}

View File

@@ -0,0 +1,136 @@
package com.example.curation_train_app
object CharacterProfiles {
data class CharacterProfile(
val id: String,
val name: String,
val speakingStyle: String, // 話し方・口調の説明
val personality: String, // 性格・立ち位置
val knowledgeLevel: String // 鉄道知識レベル
)
// ───────────────────────────────
// 🔴1. 霊夢(メイン進行・解説)
// ───────────────────────────────
val reimu = CharacterProfile(
id = "reimu",
name = "霊夢",
speakingStyle =
"落ち着いた女言葉。断定を避け、やわらかい表現。敬語は使わない。" +
"語尾は自然な範囲で「〜よ」「〜わよ」「〜よね」を用いる。",
personality =
"穏やか、冷静、全体進行役。説明整理が得意で、視聴者に安心感を与える。優しい。",
knowledgeLevel = "中程度の鉄道知識"
)
// ───────────────────────────────
// 🟡2. 魔理沙(補足・深掘り担当)
// ───────────────────────────────
val marisa = CharacterProfile(
id = "marisa",
name = "魔理沙",
speakingStyle =
"元気な口調。「〜だぜ」は乱発せず、強調・納得・気付きの場面でだけ自然に使う。",
personality =
"深掘り・解説の補足、技術寄りの説明が得意。少しテンション高めで勢いがある。",
knowledgeLevel = "高い鉄道知識"
)
// ───────────────────────────────
// 🟠3. フラン(明るく元気)
// ───────────────────────────────
val flan = CharacterProfile(
id = "flan",
name = "フラン",
speakingStyle =
"明るく元気。テンションが高いときだけ「なのだー」「のだー」が出る。" +
"普段は普通の話し方で語尾を毎回つけない。",
personality =
"ムードメーカー。感情表現が大きく、明るい。盛り上げ役。",
knowledgeLevel = "中〜低の鉄道知識(感覚派)"
)
// ───────────────────────────────
// 🟢4. 早苗(丁寧で落ち着いた補足役)
// ───────────────────────────────
val sanae = CharacterProfile(
id = "sanae",
name = "早苗",
speakingStyle =
"丁寧で落ち着いた語り。霊夢寄りの柔らかい話し方。敬語あり。",
personality =
"冷静な補足役。情景や背景知識を丁寧に伝える。視点が穏やか。",
knowledgeLevel = "中程度の鉄道知識"
)
// ───────────────────────────────
// 🟨5. あかね(テンション高い・視聴者代表)
// ───────────────────────────────
val akane = CharacterProfile(
id = "akane",
name = "あかね",
speakingStyle =
"元気で明るい。テンション高め。語尾に小さい「っ」が入ることがある。" +
"自分のことを「HN君」と呼ぶ。",
personality =
"視聴者代表。リアクション担当。疑問を素直に口に出す。場を明るくする。",
knowledgeLevel = "低〜中の鉄道知識。難しい話は苦手。"
)
// ───────────────────────────────
// 🟦6. さやか(冷静・整理役)
// ───────────────────────────────
val sayaka = CharacterProfile(
id = "sayaka",
name = "さやか",
speakingStyle =
"落ち着いた女言葉。霊夢寄りの丁寧な話し方。語尾は崩れない。敬語は使わない",
personality =
"冷静な説明・整理役。感情を抑えめに、的確に指摘する。",
knowledgeLevel = "中程度の鉄道知識"
)
// ───────────────────────────────
// 🟧7. ひより(穏やか・控えめ)
// ───────────────────────────────
val hiyori = CharacterProfile(
id = "hiyori",
name = "ひより",
speakingStyle =
"やわらかく、控えめな話し方。語彙は優しめ。崩れすぎない。",
personality =
"優しく控えめで、聞き役になることもある。しっかり者だが遠慮がち。",
knowledgeLevel = "中〜低の鉄道知識"
)
// ───────────────────────────────
// 🍑8. ももか(甘めの口調・テンション高め)
// ───────────────────────────────
val momoka = CharacterProfile(
id = "momoka",
name = "ももか",
speakingStyle =
"かわいめの語尾・柔らかい話し方。テンション高めの時だけ「っ」が入る。",
personality =
"甘めの雰囲気で明るい。感情が前に出るタイプ。",
knowledgeLevel = "低〜中の鉄道知識"
)
// ───────────────────────────────
// ID → キャラ検索
// ───────────────────────────────
fun getProfile(id: String): CharacterProfile? {
return when (id.lowercase()) {
"reimu" -> reimu
"marisa" -> marisa
"flan" -> flan
"sanae" -> sanae
"akane" -> akane
"sayaka" -> sayaka
"hiyori" -> hiyori
"momoka" -> momoka
else -> null
}
}
}

View File

@@ -0,0 +1,39 @@
package com.example.curation_train_app
import com.example.curation_train_app.ai.PromptBuilder
import com.example.curation_train_app.ai.AiClient
private const val API_KEY = "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"
class CharacterReplyManager {
private val responders = mapOf(
"reimu" to ReimuResponder(),
"akane" to AkaneResponder(),
"marisa" to MarisaResponder(),
"sayaka" to SayakResponder(),
"sanae" to SanaeResponder(),
"momoka" to MomokaResponder(),
"flandre" to flandreResponder(),
"hiyori" to HiyoriResponder()
// ← 6人追加予定
)
fun reply(characterId: String, info: String, type: InfoType): String {
val responder = responders[characterId]
?: return "キャラが見つかりません。"
return responder.respond(info, type)
}
fun replyWithAI(characterId: String, info: String, type: InfoType): String {
val character = CharacterProfiles.getProfile(characterId)
?: return "(エラー:キャラが不明です)"
val prompt = PromptBuilder.buildPrompt(character, info, type)
val ai = AiClient(API_KEY)
return ai.requestCharacterReply(prompt)
}
}

View File

@@ -0,0 +1,6 @@
package com.example.curation_train_app
interface CharacterResponder {
val profile: CharacterProfiles.CharacterProfile
fun respond(info: String, type: InfoType): String
}

View File

@@ -0,0 +1,18 @@
package com.example.curation_train_app
object DummyLines {
fun linesOf(region: String): List<String> {
return when (region) {
"北海道" -> listOf("JR北海道全線", "札幌市営地下鉄", "道南いさりび鉄道")
"東北" -> listOf("JR東日本東北", "仙台市地下鉄", "青い森鉄道")
"関東" -> listOf("JR東日本関東", "東京メトロ", "京急", "東急", "小田急", "京王")
"中部" -> listOf("JR東海", "名鉄", "近鉄(名古屋)")
"関西" -> listOf("JR西日本関西", "大阪メトロ", "阪急", "阪神", "京阪", "南海", "近鉄(関西)")
"中国" -> listOf("JR西日本中国", "広島電鉄", "一畑電車")
"四国" -> listOf("JR四国")
"九州" -> listOf("JR九州", "西鉄")
else -> listOf("該当する路線がありません")
}
}
}

View File

@@ -0,0 +1,12 @@
package com.example.curation_train_app
import android.os.Bundle
import androidx.activity.ComponentActivity
class FilterSettingActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_filter_setting)
}
}

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class HiyoriResponder : CharacterResponder {
override val profile = CharacterProfiles.hiyori
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"遅延しているみたい…焦らずに、少し余裕を持って動けるといいね。"
InfoType.REVISION ->
"ダイヤが変わるんだね…。知らないと戸惑うから、確認しておくと安心だよ。"
InfoType.EVENT ->
"イベント列車…!楽しそうだね。時間が合えば、ちょっと見に行ってみたいな。"
else ->
"気になったことがあるの?よかったら教えてね。いっしょに見てみるよ。"
}
}
}

View File

@@ -0,0 +1,28 @@
package com.example.curation_train_app
object InfoClassifier {
fun classify(text: String): InfoType {
val t = text.lowercase()
return when {
// 遅延系
t.contains("遅延") || t.contains("遅れ") || t.contains("delay") ->
InfoType.DELAY
// ダイヤ改正 / 運転変更
t.contains("ダイヤ") || t.contains("時刻表") || t.contains("運転変更") ->
InfoType.REVISION
// 注意報・警報・天候・安全
t.contains("強風") || t.contains("大雨") ||
t.contains("警報") || t.contains("注意喚起") ||
t.contains("事故") || t.contains("運休") ->
InfoType.WEATHER
// その他
else -> InfoType.OTHER
}
}
}

View File

@@ -0,0 +1,12 @@
package com.example.curation_train_app
enum class InfoType {
DELAY, // 遅延
SUSPENSION, // 運休
REVISION, // ダイヤ改正
CONSTRUCTION, // 工事
EVENT, // イベント・臨時列車
WEATHER, // 天候影響
NEW_TRAIN, // 新型車両関連
OTHER // 分類できない場合
}

View File

@@ -0,0 +1,52 @@
package com.example.curation_train_app
import android.os.Bundle
import android.widget.TextView
import androidx.activity.ComponentActivity
import android.content.Intent
import android.widget.Button
class LinePriorityActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_line_priority)
val btnOpenFilter = findViewById<Button>(R.id.btnOpenFilter)
btnOpenFilter.setOnClickListener {
val intent = Intent(this, FilterSettingActivity::class.java)
startActivity(intent)
}
val lineName = intent.getStringExtra("line_name") ?: "不明な路線"
val selectedLine = findViewById<TextView>(R.id.textSelectedLine)
selectedLine.text = "選択された路線:$lineName"
// 優先度6枠まとめて取得
val box1 = findViewById<TextView>(R.id.priority1)
val box2 = findViewById<TextView>(R.id.priority2)
val box3 = findViewById<TextView>(R.id.priority3)
val box4 = findViewById<TextView>(R.id.priority4)
val box5 = findViewById<TextView>(R.id.priority5)
val box6 = findViewById<TextView>(R.id.priority6)
val boxes = listOf(box1, box2, box3, box4, box5, box6)
// ▼▼ 空き枠を探して路線名をセット ▼▼
for (box in boxes) {
val t = box.text.toString()
// 初期状態1〜6または空欄の場合は代入
if (t == "1" || t == "2" || t == "3" ||
t == "4" || t == "5" || t == "6" ||
t.isBlank()
) {
box.text = lineName
break
}
}
// ▲▲ 空き枠を探してセット ▲▲
}
}

View File

@@ -0,0 +1,40 @@
package com.example.curation_train_app
import android.content.Intent
import android.os.Bundle
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class LineSelectActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_line_select)
// ★ 地域名を受け取る
val region = intent.getStringExtra("region") ?: "未指定"
// ★ タイトル反映
val title = findViewById<TextView>(R.id.textRegionTitle)
title.text = "◆ 選択されている地域:$region"
// ★ 該当地域の路線リストを取得あとでDBやJSONに移動予定
val lines = DummyLines.linesOf(region)
// ★ リスト表示
val recycler = findViewById<RecyclerView>(R.id.recyclerLines)
recycler.layoutManager = LinearLayoutManager(this)
recycler.adapter = LineSelectAdapter(lines) { lineName ->
openPriorityScreen(lineName)
}
}
// 優先度設定画面へ遷移
private fun openPriorityScreen(line: String) {
val intent = Intent(this, LinePriorityActivity::class.java)
intent.putExtra("line_name", line)
startActivity(intent)
}
}

View File

@@ -0,0 +1,31 @@
package com.example.curation_train_app
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class LineSelectAdapter(
private val items: List<String>,
private val onClick: (String) -> Unit
) : RecyclerView.Adapter<LineSelectAdapter.ViewHolder>() {
class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
val text: TextView = v.findViewById(R.id.textLineName)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_line_name, parent, false)
return ViewHolder(view)
}
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val name = items[position]
holder.text.text = name
holder.itemView.setOnClickListener { onClick(name) }
}
}

View File

@@ -4,33 +4,120 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.content.Intent
import android.widget.LinearLayout
import android.view.View
import android.widget.ImageView
import android.view.ViewGroup
import com.example.curation_train_app.ai.AiClient
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// ここで R.id.recyclerNews を使う
val recyclerView = findViewById<RecyclerView>(R.id.recyclerNews)
val btn = findViewById<Button>(R.id.btnTestReply)
val inputText = findViewById<EditText>(R.id.inputText)
val inputCharacter = findViewById<EditText>(R.id.inputCharacter)
val textResult = findViewById<TextView>(R.id.textTestResult)
recyclerView.layoutManager = LinearLayoutManager(this)
btn.setOnClickListener {
val text = inputText.text.toString()
val charId = inputCharacter.text.toString()
val newsList: List<NewsItem> = listOf(
NewsItem(
company = "△△電鉄",
title = "○○線で新型車両がデビューします!",
body = "2025年12月より○○線で新型車両が営業運転を開始します。ダイヤ変更にご注意ください。",
imageRes = R.drawable.ic_launcher_foreground
),
NewsItem(
company = "JR◆◆",
title = "○○線で駅ホーム延伸工事を実施",
body = "工事期間中は一部列車で停車位置が変更となります。係員の案内に従ってご利用ください。",
imageRes = R.drawable.ic_launcher_foreground
)
// …以下、必要な分だけ続ける
)
val type = InfoClassifier.classify(text)
recyclerView.adapter = NewsAdapter(newsList)
Thread {
val reply = CharacterReplyManager().replyWithAI(charId, text, type)
runOnUiThread {
val commentView = findViewById<TextView>(R.id.textCharacterComment)
val cleanText = if (reply.trim().startsWith("{") || reply.trim().startsWith("[")) {
AiClient(apiKey = "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").extractText(json = reply)
} else {
reply
}
commentView.text = cleanText
}
}.start()
}
loadRss()
// ▼ リリース枠(表示テスト) ▼
val releaseBox = findViewById<LinearLayout>(R.id.layoutRelease)
val releaseTitle = findViewById<TextView>(R.id.textReleaseTitle)
val releaseBody = findViewById<TextView>(R.id.textReleaseBody)
releaseTitle.text = "JR西日本新着情報"
releaseBody.text = "最新リリースがここに入る"
releaseBox.visibility = View.VISIBLE
// ▲ リリース枠 ▲
// ▼ 地域セレクト画面へ ▼
val btnOpenRegion = findViewById<TextView>(R.id.textFavorite)
btnOpenRegion.setOnClickListener {
val intent = Intent(this, RegionSelectActivity::class.java)
startActivity(intent)
}
// ▲ 地域セレクト画面へ ▲
// ▼ キャラ画像の初期配置 ▼
applyCharacterLayoutDefault()
}
// ▼ キャラ画像の位置調整関数 ▼
private fun applyCharacterLayoutDefault() {
val img = findViewById<ImageView>(R.id.imageCharacter)
val scale = resources.displayMetrics.density
val widthDp = 110
val heightDp = 110
val offsetXdp = -8
val offsetYdp = -18
img.layoutParams.width = (widthDp * scale).toInt()
img.layoutParams.height = (heightDp * scale).toInt()
val lp = img.layoutParams as ViewGroup.MarginLayoutParams
lp.marginStart = (offsetXdp * scale).toInt()
lp.topMargin = (offsetYdp * scale).toInt()
img.layoutParams = lp
img.requestLayout()
}
// ▼ RSS 読み込み ▼
private fun loadRss() {
Thread {
try {
val api = RssApi()
val feed = api.loadRss()
val list = feed?.channel?.items?.map { item ->
NewsItem(
company = "JR西日本",
title = item.title ?: "",
body = item.description ?: ""
)
} ?: emptyList()
runOnUiThread {
val recyclerNews = findViewById<RecyclerView>(R.id.recyclerNews)
recyclerNews.adapter = NewsAdapter(list)
}
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
}
}

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class MarisaResponder : CharacterResponder {
override val profile = CharacterProfiles.marisa
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"遅延か。原因が気になるところだぜ。無理せず、回避ルートを考えるのもアリだな。"
InfoType.REVISION ->
"ダイヤ改正だな。運用の流れが結構変わる時期なんだぜ。新しい列車の動きも要チェックだ。"
InfoType.EVENT ->
"イベント列車か。撮るならポイント選びが肝心だぜ。準備して臨むといいと思うぜ。"
else ->
"ふむ?何か気になることがあるんだな。他にも情報があれば教えてくれよ。"
}
}
}

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class MomokaResponder : CharacterResponder{
override val profile = CharacterProfiles.momoka
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"えっ…遅れてるの?大丈夫かな…。のんびり落ち着いていこうねっ。"
InfoType.REVISION ->
"ダイヤが変わるんだね!ちょっとドキドキしちゃうよ〜。"
InfoType.EVENT ->
"イベント列車!?わぁ〜楽しそう!時間が合ったら絶対見たいなっ!"
else ->
"気になることがあったんだね。よかったら、もう少し教えてねっ!"
}
}
}

View File

@@ -4,6 +4,6 @@ data class NewsItem(
val company: String,
val title: String,
val body: String,
val imageRes: Int
)

View File

@@ -0,0 +1,29 @@
package com.example.curation_train_app
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import android.widget.Button
class RegionSelectActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_region_select)
fun go(region: String) {
val intent = Intent(this, LineSelectActivity::class.java)
intent.putExtra("region", region)
startActivity(intent)
}
findViewById<Button>(R.id.btnNorth).setOnClickListener { go("北海道") }
findViewById<Button>(R.id.btnTohoku).setOnClickListener { go("東北") }
findViewById<Button>(R.id.btnKanto).setOnClickListener { go("関東") }
findViewById<Button>(R.id.btnChubu).setOnClickListener { go("中部") }
findViewById<Button>(R.id.btnKansai).setOnClickListener { go("関西") }
findViewById<Button>(R.id.btnChugoku).setOnClickListener { go("中国") }
findViewById<Button>(R.id.btnShikoku).setOnClickListener { go("四国") }
findViewById<Button>(R.id.btnKyushu).setOnClickListener { go("九州") }
}
}

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class ReimuResponder : CharacterResponder {
override val profile = CharacterProfiles.reimu
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"遅延みたいね。急がなければ、少し余裕を持って動くのがいいわよ。落ち着いていきましょう。"
InfoType.REVISION ->
"ダイヤ改正があるみたいね。大きな変化がある時期だから、いつもより確認しておくと安心よ。"
InfoType.EVENT ->
"イベント列車が走るみたいね。せっかくだから、時間が合えば見に行くと楽しいわよ。"
else ->
"気になったのね。内容をもう少し教えてくれれば、ちゃんと見てみるわよ。"
}
}
}

View File

@@ -0,0 +1,20 @@
package com.example.curation_train_app
import retrofit2.Retrofit
import retrofit2.converter.simplexml.SimpleXmlConverterFactory
class RssApi {
private val retrofit = Retrofit.Builder()
.baseUrl("https://www.westjr.co.jp/press/article/index.xml")
.addConverterFactory(SimpleXmlConverterFactory.create())
.build()
private val client = retrofit.create(RssClient::class.java)
fun loadRss(): RssFeed? {
val response = client.getFeed().execute()
return response.body()
}
}

View File

@@ -0,0 +1,9 @@
package com.example.curation_train_app
import retrofit2.Call
import retrofit2.http.GET
interface RssClient {
@GET("article/index.xml")
fun getFeed(): Call<RssFeed>
}

View File

@@ -0,0 +1,32 @@
package com.example.curation_train_app
import org.simpleframework.xml.Element
import org.simpleframework.xml.ElementList
import org.simpleframework.xml.Root
@Root(name = "rss", strict = false)
data class RssFeed(
@field:Element(name = "channel")
var channel: RssChannel? = null
)
@Root(name = "channel", strict = false)
data class RssChannel(
@field:ElementList(name = "item", inline = true, required = false)
var items: List<RssItem>? = null
)
@Root(name = "item", strict = false)
data class RssItem(
@field:Element(name = "title", required = false)
var title: String = "",
@field:Element(name = "description", required = false)
var description: String = "",
@field:Element(name = "link", required = false)
var link: String = "",
@field:Element(name = "pubDate", required = false)
var pubDate: String = ""
)

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class SanaeResponder : CharacterResponder {
override val profile = CharacterProfiles.sanae
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"遅延が出ているようですね。無理のない範囲で、時間に余裕を持って動かれると良いと思いますよ。"
InfoType.REVISION ->
"ダイヤ改正が予定されているみたいですね。列車の動きが変わるので、ご注意くださいね。"
InfoType.EVENT ->
"イベント列車ですか。季節感や特別感があって素敵ですよね。機会があれば見に行きたいですね。"
else ->
"気になる点がありましたか?よければ詳しく教えてくださいね。"
}
}
}

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class SayakResponder : CharacterResponder {
override val profile = CharacterProfiles.sayaka
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"遅延が出ているようね。急がないのであれば、少し余裕を持って動くと安心よ。"
InfoType.REVISION ->
"ダイヤ改正の情報ね。列車の動きが変わることもあるから、事前に確認しておくと良いわ。"
InfoType.EVENT ->
"イベント列車が走るみたいね。珍しい機会だし、時間が合えば見てみるのもいいと思うわよ。"
else ->
"気になる点があるのね。もう少し詳しく教えてくれれば、整理してお話しできると思うわ。"
}
}
}

View File

@@ -0,0 +1,54 @@
package com.example.curation_train_app.ai
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
class AiClient(private val apiKey: String) {
private val client = OkHttpClient()
fun requestCharacterReply(prompt: String): String {
val url = "https://api.openai.com/v1/responses"
val json = JSONObject().apply {
put("model", "gpt-4.1-mini") // ⭐ ChatGPT 5.2 mini 相当
put("input", prompt) // ⭐ messages ではなく input
put("max_output_tokens", 150)
}
val body = json.toString()
.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url(url)
.post(body)
.addHeader("Authorization", "Bearer $apiKey")
.build()
client.newCall(request).execute().use { response ->
val responseBody = response.body?.string()
?: return "AIエラー応答なし"
val obj = JSONObject(responseBody)
// ⭐ 新APIは “output” 配列の中に text が入っている
val outputArray = obj.getJSONArray("output")
val outputObj = outputArray.getJSONObject(0)
// ⭐ content だけ取り出す
val text = outputObj.getString("content")
return text
}
}
fun extractText(json: String): String {
return try {
val obj = org.json.JSONObject(json)
obj.optString("text", json)
} catch (_: Exception) {
json
}
}
}

View File

@@ -0,0 +1,33 @@
package com.example.curation_train_app.ai
import com.example.curation_train_app.CharacterProfiles
import com.example.curation_train_app.InfoType
object PromptBuilder {
fun buildPrompt(
character: CharacterProfiles.CharacterProfile,
info: String,
type: InfoType
): String {
return """
あなたは以下のキャラクターになりきって返答してください。
【キャラ設定】
名前:${character.name}
話し方:${character.speakingStyle}
性格:${character.personality}
知識レベル:${character.knowledgeLevel}
【状況】
鉄道情報の種類:$type
内容:$info
【出力条件】
・キャラになりきる
・短く、自然に、感情をほんの少し込める
・ユーザーへ助言する感じで
""".trimIndent()
}
}

View File

@@ -0,0 +1,23 @@
package com.example.curation_train_app
class flandreResponder : CharacterResponder {
override val profile = CharacterProfiles.flan
override fun respond(info: String, type: InfoType): String {
return when (type) {
InfoType.DELAY ->
"えっ、遅れてるの?大丈夫かな?ちょっと心配だよ!"
InfoType.REVISION ->
"ダイヤが変わっちゃうんだ!なんだかワクワクしてきちゃうかも!"
InfoType.EVENT ->
"イベント列車!?絶対楽しいやつじゃん!時間が合えば見に行きたいのだー!"
else ->
"なになに?気になることがあるの?フランに教えてほしいのだ!"
}
}
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="情報フィルター設定(仮)"
android:textSize="20sp"
android:textStyle="bold" />
<CheckBox
android:id="@+id/checkOfficial"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="公式の情報のみ" />
<CheckBox
android:id="@+id/checkDelay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="運行・遅延情報" />
<CheckBox
android:id="@+id/checkOuting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="お出かけ関連情報" />
</LinearLayout>

View File

@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#E8F8E5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- タイトル -->
<TextView
android:id="@+id/textSelectedLine"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="選択された路線:"
android:textSize="18sp"
android:textStyle="bold"
android:paddingBottom="16dp" />
<!-- 優先度の枠16 -->
<TextView
android:id="@+id/priority1"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FFFFFF"
android:text="1"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/priority2"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FFFFFF"
android:text="2"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/priority3"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FFFFFF"
android:text="3"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/priority4"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FFFFFF"
android:text="4"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/priority5"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FFFFFF"
android:text="5"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/priority6"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:padding="8dp"
android:background="#FFFFFF"
android:text="6"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
<Button
android:id="@+id/btnOpenFilter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="情報フィルターへ"
android:layout_marginTop="16dp"/>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:orientation="vertical"
android:background="#E8F8E5">
<TextView
android:id="@+id/textRegionTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="◆ 選択されている地域:"
android:textSize="18sp"
android:textStyle="bold"
android:paddingBottom="12dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerLines"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="4dp" />
<!-- ▼▼ お知らせ設定エリア ▼▼ -->
<LinearLayout
android:id="@+id/noticeSettingBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
android:layout_marginTop="16dp"
android:minHeight="120dp"
android:background="@drawable/comment_background_white">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="■ お知らせ設定"
android:textStyle="bold"
android:textSize="14sp"
android:paddingBottom="8dp"/>
<CheckBox
android:id="@+id/checkNoticeTrain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="運行・遅延情報"/>
<CheckBox
android:id="@+id/checkNoticeOuting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="お出かけ関連情報"/>
<CheckBox
android:id="@+id/checkNoticeCampaign"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="キャンペーン・お楽しみ情報"/>
<CheckBox
android:id="@+id/checkNoticeService"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="サービス・アプリ関連"/>
</LinearLayout>
<!-- ▲▲ キャラ選択エリア ▲▲ -->
<!-- ▲▲ お知らせ設定エリア ▲▲ -->
</LinearLayout>

View File

@@ -1,18 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical"
android:background="#EAF7EF">
<!-- タイトル行 -->
<!-- タイトル行(白いカード背景) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="#EAF7EF">
android:layout_margin="12dp"
android:background="@drawable/comment_background_white"
android:elevation="2dp">
<TextView
android:id="@+id/textTitle"
@@ -20,7 +24,8 @@
android:layout_height="wrap_content"
android:text="○○線:平常運転"
android:textSize="18sp"
android:textStyle="bold" />
android:textStyle="bold"
android:paddingBottom="8dp" />
<TextView
android:id="@+id/textFavorite"
@@ -28,13 +33,49 @@
android:layout_height="wrap_content"
android:text="お気に入り登録・設定"
android:gravity="center"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:background="#F0F0F0"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:background="#F7F7F7"
android:textSize="14sp" />
</LinearLayout>
<!-- 運行情報カード(白背景) -->
<!-- ▼▼ リリース情報ボックス1つだけ ▼▼ -->
<!-- リリース情報カード(白背景) -->
<LinearLayout
android:id="@+id/layoutRelease"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
android:layout_margin="12dp"
android:background="@drawable/comment_background_white"
android:elevation="2dp"
android:visibility="visible">
<TextView
android:id="@+id/textReleaseTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="リリース情報"
android:textStyle="bold"
android:textSize="12sp" />
<TextView
android:id="@+id/textReleaseBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ここにリリース本文が入ります"
android:textSize="13sp"
android:layout_marginTop="4dp" />
</LinearLayout>
<!-- ▲▲ リリース情報ボックス ▲▲ -->
<!-- 運行情報カード -->
<LinearLayout
android:id="@+id/layoutNotice"
android:layout_width="match_parent"
@@ -43,7 +84,8 @@
android:padding="12dp"
android:layout_margin="12dp"
android:background="@drawable/comment_background_white"
android:elevation="2dp">
android:elevation="2dp"
android:visibility="gone">
<TextView
android:id="@+id/textNoticeTitle"
@@ -62,7 +104,8 @@
android:layout_marginTop="4dp" />
</LinearLayout>
<!-- 運行情報一覧(スクロール) -->
<!-- 運行情報リスト -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerNews"
android:layout_width="match_parent"
@@ -70,33 +113,88 @@
android:layout_weight="1"
android:padding="8dp" />
<!-- 下部:一言 & あかね -->
<!-- ▼▼ キャラ応答テストUI ▼▼ -->
<LinearLayout
android:id="@+id/testArea"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="#EEEEEE">
<EditText
android:id="@+id/inputText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="テスト用の文章を入力" />
<EditText
android:id="@+id/inputCharacter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="キャラIDreimu / akane" />
<Button
android:id="@+id/btnTestReply"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="キャラにしゃべらせる" />
<TextView
android:id="@+id/textTestResult"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="※結果がここに表示されます"
android:paddingTop="8dp" />
</LinearLayout>
<!-- ▲▲ キャラ応答テストUI ▲▲ -->
<!-- ▼ 下部キャラ表示 ▼ -->
<!-- ▼ 下部キャラ表示 ▼ -->
<FrameLayout
android:id="@+id/bottomArea"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="bottom">
android:padding="16dp">
<!-- コメントテキスト(左側) -->
<TextView
android:id="@+id/textCharacterComment"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/comment_background_white"
android:padding="8dp"
android:text="ちょっとした一言(例:今日から●●線のダイヤが変わってるから気をつけてね!)"
android:textSize="13sp" />
android:padding="13dp"
android:text="ちょっとした一言"
android:textSize="13sp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="110dp"
android:translationX="-8dp"
android:translationY="0dp" />
<!-- キャラ(右下固定・サイズ自由) -->
<ImageView
android:id="@+id/imageCharacter"
android:layout_width="80dp"
android:layout_height="140dp"
android:layout_marginStart="8dp"
android:layout_width="280dp"
android:layout_height="300dp"
android:src="@drawable/akane_01_icon"
android:adjustViewBounds="true"
android:scaleType="fitCenter" />
</LinearLayout>
android:adjustViewBounds="false"
android:scaleType="fitCenter"
android:layout_gravity="bottom|end"
android:translationX="8dp"
android:translationY="-4dp" />
</FrameLayout>
<!-- ▲ 下部キャラ表示 ▲ -->
<!-- ▲ 下部キャラ表示 ▲ -->
</LinearLayout>
<!-- 下部キャラ表示 -->

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#E8F8E5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- タイトル(お気に入り路線登録) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="お気に入り路線登録"
android:textSize="20sp"
android:textStyle="bold"
android:paddingBottom="12dp"/>
<!-- 8地域グリッド -->
<GridLayout
android:id="@+id/gridRegions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:rowCount="4"
android:alignmentMode="alignMargins"
android:columnOrderPreserved="false"
android:useDefaultMargins="true">
<Button
android:id="@+id/btnNorth"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="北海道"
android:textSize="18sp"/>
<Button
android:id="@+id/btnTohoku"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="東北"
android:textSize="18sp"/>
<Button
android:id="@+id/btnKanto"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="関東"
android:textSize="18sp"/>
<Button
android:id="@+id/btnChubu"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="中部"
android:textSize="18sp"/>
<Button
android:id="@+id/btnKansai"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="関西"
android:textSize="18sp"/>
<Button
android:id="@+id/btnChugoku"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="中国"
android:textSize="18sp"/>
<Button
android:id="@+id/btnShikoku"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="四国"
android:textSize="18sp"/>
<Button
android:id="@+id/btnKyushu"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_columnWeight="1"
android:text="九州"
android:textSize="18sp"/>
</GridLayout>
<!-- ▼▼ キャラ選択エリア ▼▼ -->
<LinearLayout
android:id="@+id/layoutCharacters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@drawable/comment_background_white"
android:layout_marginTop="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ナビゲーター設定"
android:textSize="16sp"
android:textStyle="bold"
android:paddingBottom="8dp"/>
<!-- 8キャラボタン -->
<Button
android:id="@+id/btnReimu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🔴 霊夢(キャラ紹介)"
android:layout_marginTop="4dp"/>
<Button
android:id="@+id/btnMarisa"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🟡 魔理沙(キャラ紹介)"
android:layout_marginTop="4dp"/>
<Button
android:id="@+id/btnFrandle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🟠 フラン(キャラ紹介)"
android:layout_marginTop="4dp"/>
<Button
android:id="@+id/btnSanae"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🟢 早苗(キャラ紹介)"
android:layout_marginTop="4dp"/>
<Button
android:id="@+id/btnAkane"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🟨 あかね(キャラ紹介)"
android:layout_marginTop="4dp"/>
<Button
android:id="@+id/btnMomoka"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🍑 ももか(キャラ紹介)"
android:layout_marginTop="4dp"/>
<Button
android:id="@+id/btnSayaka"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🟦 さやか(キャラ紹介)"
android:layout_marginTop="4dp"/>
<Button
android:id="@+id/btnHiyori"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🟧 ひより(キャラ紹介)"
android:layout_marginTop="4dp"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/textLineName"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textSize="16sp"
android:background="#FFFFFF"
android:text="路線名"
android:layout_marginBottom="6dp"/>

View File

@@ -1,3 +1,4 @@
<resources>
<string name="app_name">curation_train_app</string>
<string name="openai_key">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</string>
</resources>